mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
core&ui: add clarification (#669)
This commit is contained in:
1 parent
bc4f5115d2
commit
c8c8063240
14 files changed
+373
-80
No files matched your search
@@ -148,6 +148,9 @@ Contact Us: 联系我们
|
|||||||
Contact: 联系
|
Contact: 联系
|
||||||
content: 内容
|
content: 内容
|
||||||
Content: 内容
|
Content: 内容
|
||||||
|
Contest {0} has a new clarification about {1}, please go to contest management to reply.: 比赛 {0} 有一条关于 {1} 的新问答,请前往比赛管理页面回复。
|
||||||
|
Contest {0} jury replied to your clarification, please go to contest page to view.: 比赛 {0} 的裁判回复了您的问答,请前往比赛页面查看。
|
||||||
|
Contest Clarifications: 问答
|
||||||
Contest Management: 比赛管理
|
Contest Management: 比赛管理
|
||||||
Contest scoreboard is not visible.: 当前比赛成绩表隐藏,暂不可显示。
|
Contest scoreboard is not visible.: 当前比赛成绩表隐藏,暂不可显示。
|
||||||
Contest Scoreboard: 比赛成绩表
|
Contest Scoreboard: 比赛成绩表
|
||||||
@@ -342,6 +345,7 @@ Formula blocks: 公式区块
|
|||||||
fs_upload: 上传文件
|
fs_upload: 上传文件
|
||||||
Gender Visibility: 性别可见性
|
Gender Visibility: 性别可见性
|
||||||
Gender: 性别
|
Gender: 性别
|
||||||
|
General Issue: 一般问题
|
||||||
Hard Deadline: 最终截止时间
|
Hard Deadline: 最终截止时间
|
||||||
Hash: 散列
|
Hash: 散列
|
||||||
Have ALL PERMISSIONS in this domain: 在此域中拥有全部权限
|
Have ALL PERMISSIONS in this domain: 在此域中拥有全部权限
|
||||||
@@ -520,6 +524,7 @@ Oh, the user doesn't have any contributions!: 啊哦,这个用户还没贡献
|
|||||||
Oh, the user hasn't created any discussions yet!: 这个用户还没有发布过讨论
|
Oh, the user hasn't created any discussions yet!: 这个用户还没有发布过讨论
|
||||||
Oh, the user hasn't submitted yet!: 这个用户还没有交过题 _(:зゝ∠)_
|
Oh, the user hasn't submitted yet!: 这个用户还没有交过题 _(:зゝ∠)_
|
||||||
Oh, there are no tasks that match the filter!: 喔,目前没有符合过滤条件的任务。
|
Oh, there are no tasks that match the filter!: 喔,目前没有符合过滤条件的任务。
|
||||||
|
Oh, there is no clarification!: 喔,目前没有提问!
|
||||||
Oh, there is no task in the queue!: 喔,队列中目前没有任务。
|
Oh, there is no task in the queue!: 喔,队列中目前没有任务。
|
||||||
Ok: 确定
|
Ok: 确定
|
||||||
Only A-Z, a-z, 0-9 and _ are accepted: 只接受 A-Z, a-z, 0-9 和 _
|
Only A-Z, a-z, 0-9 and _ are accepted: 只接受 A-Z, a-z, 0-9 和 _
|
||||||
@@ -691,6 +696,8 @@ Select User: 选择用户
|
|||||||
Selected categories: 已选标签
|
Selected categories: 已选标签
|
||||||
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 Broadcast Message: 发送公告
|
||||||
|
Send Clarification Request: 提问
|
||||||
Send Code after acceptance: 通过题目后发送源代码
|
Send Code after acceptance: 通过题目后发送源代码
|
||||||
Send Message: 发送站内信息
|
Send Message: 发送站内信息
|
||||||
Send Password Reset Email: 发送密码重置邮件
|
Send Password Reset Email: 发送密码重置邮件
|
||||||
@@ -750,6 +757,7 @@ Storage engine endPoint: 存储桶 endPoint
|
|||||||
Storage engine region: 存储桶地域
|
Storage engine region: 存储桶地域
|
||||||
Storage engine secret: 存储桶 secretKey
|
Storage engine secret: 存储桶 secretKey
|
||||||
Student ID: 学号
|
Student ID: 学号
|
||||||
|
Subject: 主题
|
||||||
Submission Statistics: 递交统计
|
Submission Statistics: 递交统计
|
||||||
Submission: 递交
|
Submission: 递交
|
||||||
Submissions: 递交
|
Submissions: 递交
|
||||||
@@ -760,6 +768,7 @@ Sync problem filelist from s3 service: 从 S3 同步题目文件列表。
|
|||||||
Tags: 标签
|
Tags: 标签
|
||||||
Target: 目标
|
Target: 目标
|
||||||
Technical Information: 技术信息
|
Technical Information: 技术信息
|
||||||
|
Technical Issue: 技术问题
|
||||||
Temporary session: 临时会话
|
Temporary session: 临时会话
|
||||||
Terms of Service: 服务条款
|
Terms of Service: 服务条款
|
||||||
Test data comes from: 测试数据来自
|
Test data comes from: 测试数据来自
|
||||||
|
|||||||
@@ -256,12 +256,13 @@ export class ContestProblemListHandler extends ContestDetailBaseHandler {
|
|||||||
async get(domainId: string, tid: ObjectId) {
|
async get(domainId: string, tid: ObjectId) {
|
||||||
if (contest.isNotStarted(this.tdoc)) throw new ContestNotLiveError(domainId, tid);
|
if (contest.isNotStarted(this.tdoc)) throw new ContestNotLiveError(domainId, tid);
|
||||||
if (!this.tsdoc?.attend && !contest.isDone(this.tdoc)) throw new ContestNotAttendedError(domainId, tid);
|
if (!this.tsdoc?.attend && !contest.isDone(this.tdoc)) throw new ContestNotAttendedError(domainId, tid);
|
||||||
const [pdict, udict] = await Promise.all([
|
const [pdict, udict, tcdocs] = await Promise.all([
|
||||||
problem.getList(domainId, this.tdoc.pids, true, true, problem.PROJECTION_CONTEST_LIST),
|
problem.getList(domainId, this.tdoc.pids, true, true, problem.PROJECTION_CONTEST_LIST),
|
||||||
user.getList(domainId, [this.tdoc.owner, this.user._id]),
|
user.getList(domainId, [this.tdoc.owner, this.user._id]),
|
||||||
|
contest.getMultiClarification(domainId, tid, this.user._id),
|
||||||
]);
|
]);
|
||||||
this.response.body = {
|
this.response.body = {
|
||||||
pdict, psdict: {}, udict, rdict: {}, tdoc: this.tdoc, tsdoc: this.tsdoc,
|
pdict, psdict: {}, udict, rdict: {}, tdoc: this.tdoc, tsdoc: this.tsdoc, tcdocs,
|
||||||
};
|
};
|
||||||
this.response.template = 'contest_problemlist.html';
|
this.response.template = 'contest_problemlist.html';
|
||||||
if (!this.tsdoc) return;
|
if (!this.tsdoc) return;
|
||||||
@@ -282,6 +283,24 @@ export class ContestProblemListHandler extends ContestDetailBaseHandler {
|
|||||||
for (const i of psdocs) this.response.body.rdict[i.rid] = { _id: i.rid };
|
for (const i of psdocs) this.response.body.rdict[i.rid] = { _id: i.rid };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@param('tid', Types.ObjectId)
|
||||||
|
@param('content', Types.Content)
|
||||||
|
@param('subject', Types.Int)
|
||||||
|
async postClarification(domainId: string, tid: ObjectId, content: string, subject: number) {
|
||||||
|
if (!this.tsdoc?.attend) throw new ContestNotAttendedError(domainId, tid);
|
||||||
|
if (!contest.isOngoing(this.tdoc)) throw new ContestNotLiveError(domainId, tid);
|
||||||
|
await this.limitRate('add_discussion', 3600, 60);
|
||||||
|
await contest.addClarification(domainId, tid, this.user._id, content, this.request.ip, subject);
|
||||||
|
if (!this.user.own(this.tdoc)) {
|
||||||
|
await Promise.all([this.tdoc.owner, ...this.tdoc.maintainer].map((uid) => message.send(1, uid, JSON.stringify({
|
||||||
|
message: 'Contest {0} has a new clarification about {1}, please go to contest management to reply.',
|
||||||
|
params: [this.tdoc.title, subject > 0 ? `#${this.tdoc.pids.indexOf(subject) + 1}` : 'the contest'],
|
||||||
|
url: this.url('contest_manage', { tid }),
|
||||||
|
}), message.FLAG_I18N | message.FLAG_UNREAD)));
|
||||||
|
}
|
||||||
|
this.back();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ContestScoreboardHandler extends ContestDetailBaseHandler {
|
export class ContestScoreboardHandler extends ContestDetailBaseHandler {
|
||||||
@@ -558,12 +577,15 @@ export class ContestCodeHandler extends Handler {
|
|||||||
export class ContestManagementHandler extends ContestManagementBaseHandler {
|
export class ContestManagementHandler extends ContestManagementBaseHandler {
|
||||||
@param('tid', Types.ObjectId)
|
@param('tid', Types.ObjectId)
|
||||||
async get(domainId: string, tid: ObjectId) {
|
async get(domainId: string, tid: ObjectId) {
|
||||||
|
const tcdocs = await contest.getMultiClarification(domainId, tid);
|
||||||
this.response.body = {
|
this.response.body = {
|
||||||
tdoc: this.tdoc,
|
tdoc: this.tdoc,
|
||||||
tsdoc: this.tsdoc,
|
tsdoc: this.tsdoc,
|
||||||
owner_udoc: await user.getById(domainId, this.tdoc.owner),
|
owner_udoc: await user.getById(domainId, this.tdoc.owner),
|
||||||
pdict: await problem.getList(domainId, this.tdoc.pids, true, true, problem.PROJECTION_CONTEST_LIST),
|
pdict: await problem.getList(domainId, this.tdoc.pids, true, true, problem.PROJECTION_CONTEST_LIST),
|
||||||
files: sortFiles(this.tdoc.files || []),
|
files: sortFiles(this.tdoc.files || []),
|
||||||
|
udict: await user.getListForRender(domainId, tcdocs.map((i) => i.owner)),
|
||||||
|
tcdocs,
|
||||||
urlForFile: (filename: string) => this.url('contest_file_download', { tid, filename }),
|
urlForFile: (filename: string) => this.url('contest_file_download', { tid, filename }),
|
||||||
};
|
};
|
||||||
this.response.pjax = 'partials/files.html';
|
this.response.pjax = 'partials/files.html';
|
||||||
@@ -572,11 +594,28 @@ export class ContestManagementHandler extends ContestManagementBaseHandler {
|
|||||||
|
|
||||||
@param('tid', Types.ObjectId)
|
@param('tid', Types.ObjectId)
|
||||||
@param('content', Types.Content)
|
@param('content', Types.Content)
|
||||||
async postBroadcast(domainId: string, tid: ObjectId, content: string) {
|
@param('did', Types.ObjectId, true)
|
||||||
const tsdocs = await contest.getMultiStatus(domainId, { docId: tid }).toArray();
|
@param('subject', Types.Int, true)
|
||||||
const uids = Array.from<number>(new Set(tsdocs.map((tsdoc) => tsdoc.uid)));
|
async postClarification(domainId: string, tid: ObjectId, content: string, did: ObjectId, subject = 0) {
|
||||||
const flag = contest.isOngoing(this.tdoc) ? message.FLAG_ALERT : message.FLAG_UNREAD;
|
if (did) {
|
||||||
await Promise.all(uids.map((uid) => message.send(this.user._id, uid, content, flag)));
|
const tcdoc = await contest.getClarification(domainId, did);
|
||||||
|
await Promise.all([
|
||||||
|
contest.addClarificationReply(domainId, did, 0, content, this.request.ip),
|
||||||
|
message.send(1, tcdoc.owner, JSON.stringify({
|
||||||
|
message: 'Contest {0} jury replied to your clarification, please go to contest page to view.',
|
||||||
|
params: [this.tdoc.title],
|
||||||
|
url: this.url('contest_problemlist', { tid }),
|
||||||
|
}), message.FLAG_I18N | message.FLAG_ALERT),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
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([
|
||||||
|
contest.addClarification(domainId, tid, 0, content, this.request.ip, subject),
|
||||||
|
...uids.map((uid) => message.send(1, uid, content, flag)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
this.back();
|
this.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ export class ProblemMainHandler extends Handler {
|
|||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await problem.del(domainId, pid);
|
await problem.del(domainId, pid);
|
||||||
i++;
|
i++;
|
||||||
this.progress(`Deleting: (${i}/${pids.length})`);
|
this.progress('Deleting: ({0}/{1})', [i, pids.length]);
|
||||||
}
|
}
|
||||||
this.back();
|
this.back();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -493,6 +493,18 @@ export interface DiscussionTailReplyDoc {
|
|||||||
editor?: number;
|
editor?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ContestClarificationDoc extends Document {
|
||||||
|
docType: document['TYPE_CONTEST_CLARIFICATION'];
|
||||||
|
docId: ObjectId;
|
||||||
|
parentType: document['TYPE_CONTEST'];
|
||||||
|
parentId: ObjectId;
|
||||||
|
// 0: contest -1: technique [pid]: problem
|
||||||
|
subject: number;
|
||||||
|
ip: string;
|
||||||
|
content: string;
|
||||||
|
reply: DiscussionTailReplyDoc[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface TokenDoc {
|
export interface TokenDoc {
|
||||||
_id: string,
|
_id: string,
|
||||||
tokenType: number,
|
tokenType: number,
|
||||||
|
|||||||
@@ -911,6 +911,37 @@ export async function getScoreboard(
|
|||||||
return [tdoc, rows, udict, pdict];
|
return [tdoc, rows, udict, pdict];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function addClarification(
|
||||||
|
domainId: string, tid: ObjectId, owner: number, content: string,
|
||||||
|
ip: string, subject = 0,
|
||||||
|
) {
|
||||||
|
return document.add(
|
||||||
|
domainId, content, owner, document.TYPE_CONTEST_CLARIFICATION,
|
||||||
|
null, document.TYPE_CONTEST, tid, { ip, subject },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addClarificationReply(
|
||||||
|
domainId: string, did: ObjectId, owner: number,
|
||||||
|
content: string, ip: string,
|
||||||
|
) {
|
||||||
|
return document.push(
|
||||||
|
domainId, document.TYPE_CONTEST_CLARIFICATION, did,
|
||||||
|
'reply', { content, owner, ip },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getClarification(domainId: string, did: ObjectId) {
|
||||||
|
return document.get(domainId, document.TYPE_CONTEST_CLARIFICATION, did);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMultiClarification(domainId: string, tid: ObjectId, owner = 0) {
|
||||||
|
return document.getMulti(
|
||||||
|
domainId, document.TYPE_CONTEST_CLARIFICATION,
|
||||||
|
{ parentType: document.TYPE_CONTEST, parentId: tid, ...(owner ? { owner: { $in: [owner, 0] } } : {}) },
|
||||||
|
).sort('_id', -1).toArray();
|
||||||
|
}
|
||||||
|
|
||||||
export const statusText = (tdoc: Tdoc, tsdoc?: any) => (
|
export const statusText = (tdoc: Tdoc, tsdoc?: any) => (
|
||||||
isNew(tdoc)
|
isNew(tdoc)
|
||||||
? 'New'
|
? 'New'
|
||||||
@@ -948,6 +979,10 @@ global.Hydro.model.contest = {
|
|||||||
canShowScoreboard,
|
canShowScoreboard,
|
||||||
canViewHiddenScoreboard,
|
canViewHiddenScoreboard,
|
||||||
getScoreboard,
|
getScoreboard,
|
||||||
|
addClarification,
|
||||||
|
addClarificationReply,
|
||||||
|
getClarification,
|
||||||
|
getMultiClarification,
|
||||||
isNew,
|
isNew,
|
||||||
isUpcoming,
|
isUpcoming,
|
||||||
isNotStarted,
|
isNotStarted,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
} from 'mongodb';
|
} from 'mongodb';
|
||||||
import { Context } from '../context';
|
import { Context } from '../context';
|
||||||
import {
|
import {
|
||||||
Content, DiscussionDoc,
|
Content, ContestClarificationDoc, DiscussionDoc,
|
||||||
DiscussionReplyDoc, ProblemDoc, ProblemStatusDoc,
|
DiscussionReplyDoc, ProblemDoc, ProblemStatusDoc,
|
||||||
Tdoc, TrainingDoc,
|
Tdoc, TrainingDoc,
|
||||||
} from '../interface';
|
} from '../interface';
|
||||||
@@ -27,6 +27,7 @@ export const TYPE_DISCUSSION_NODE: 20 = 20;
|
|||||||
export const TYPE_DISCUSSION: 21 = 21;
|
export const TYPE_DISCUSSION: 21 = 21;
|
||||||
export const TYPE_DISCUSSION_REPLY: 22 = 22;
|
export const TYPE_DISCUSSION_REPLY: 22 = 22;
|
||||||
export const TYPE_CONTEST: 30 = 30;
|
export const TYPE_CONTEST: 30 = 30;
|
||||||
|
export const TYPE_CONTEST_CLARIFICATION: 31 = 31;
|
||||||
export const TYPE_TRAINING: 40 = 40;
|
export const TYPE_TRAINING: 40 = 40;
|
||||||
/** @deprecated use `TYPE_CONTEST` with rule `homework` instead. */
|
/** @deprecated use `TYPE_CONTEST` with rule `homework` instead. */
|
||||||
export const TYPE_HOMEWORK: 60 = 60;
|
export const TYPE_HOMEWORK: 60 = 60;
|
||||||
@@ -39,6 +40,7 @@ export interface DocType {
|
|||||||
[TYPE_DISCUSSION]: DiscussionDoc;
|
[TYPE_DISCUSSION]: DiscussionDoc;
|
||||||
[TYPE_DISCUSSION_REPLY]: DiscussionReplyDoc;
|
[TYPE_DISCUSSION_REPLY]: DiscussionReplyDoc;
|
||||||
[TYPE_CONTEST]: Tdoc;
|
[TYPE_CONTEST]: Tdoc;
|
||||||
|
[TYPE_CONTEST_CLARIFICATION]: ContestClarificationDoc;
|
||||||
[TYPE_TRAINING]: TrainingDoc;
|
[TYPE_TRAINING]: TrainingDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,6 +500,7 @@ global.Hydro.model.document = {
|
|||||||
setSub,
|
setSub,
|
||||||
|
|
||||||
TYPE_CONTEST,
|
TYPE_CONTEST,
|
||||||
|
TYPE_CONTEST_CLARIFICATION,
|
||||||
TYPE_DISCUSSION,
|
TYPE_DISCUSSION,
|
||||||
TYPE_DISCUSSION_NODE,
|
TYPE_DISCUSSION_NODE,
|
||||||
TYPE_DISCUSSION_REPLY,
|
TYPE_DISCUSSION_REPLY,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class MessageModel {
|
|||||||
static FLAG_ALERT = 2;
|
static FLAG_ALERT = 2;
|
||||||
static FLAG_RICHTEXT = 4;
|
static FLAG_RICHTEXT = 4;
|
||||||
static FLAG_INFO = 8;
|
static FLAG_INFO = 8;
|
||||||
|
static FLAG_I18N = 16;
|
||||||
|
|
||||||
static coll = db.collection('message');
|
static coll = db.collection('message');
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ class MessageModel {
|
|||||||
static async sendInfo(to: number, content: string) {
|
static async sendInfo(to: number, content: string) {
|
||||||
const _id = new ObjectId();
|
const _id = new ObjectId();
|
||||||
const mdoc: MessageDoc = {
|
const mdoc: MessageDoc = {
|
||||||
_id, from: 1, to, content, flag: MessageModel.FLAG_INFO,
|
_id, from: 1, to, content, flag: MessageModel.FLAG_INFO | MessageModel.FLAG_I18N,
|
||||||
};
|
};
|
||||||
bus.broadcast('user/message', to, mdoc);
|
bus.broadcast('user/message', to, mdoc);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,8 +183,8 @@ export class Handler extends HandlerCommon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// This is beta API, may be changed in the future.
|
// This is beta API, may be changed in the future.
|
||||||
progress(message: string) {
|
progress(message: string, params: any[]) {
|
||||||
Hydro.model.message.sendInfo(this.user._id, message);
|
Hydro.model.message.sendInfo(this.user._id, JSON.stringify({ message, params }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
|||||||
@@ -2,13 +2,24 @@ import { nanoid } from 'nanoid';
|
|||||||
import ReconnectingWebsocket from 'reconnecting-websocket';
|
import ReconnectingWebsocket from 'reconnecting-websocket';
|
||||||
import { InfoDialog } from 'vj/components/dialog';
|
import { InfoDialog } from 'vj/components/dialog';
|
||||||
import VjNotification from 'vj/components/notification/index';
|
import VjNotification from 'vj/components/notification/index';
|
||||||
import { FLAG_ALERT, FLAG_INFO, FLAG_RICHTEXT } from 'vj/constant/message';
|
import {
|
||||||
|
FLAG_ALERT, FLAG_I18N, FLAG_INFO, FLAG_RICHTEXT,
|
||||||
|
} from 'vj/constant/message';
|
||||||
import { AutoloadPage } from 'vj/misc/Page';
|
import { AutoloadPage } from 'vj/misc/Page';
|
||||||
import { i18n, tpl } from 'vj/utils';
|
import { i18n, tpl } from 'vj/utils';
|
||||||
|
|
||||||
let previous: VjNotification;
|
let previous: VjNotification;
|
||||||
const onmessage = (msg) => {
|
const onmessage = (msg) => {
|
||||||
console.log('Received message', msg);
|
console.log('Received message', msg);
|
||||||
|
if (msg.mdoc.flag & FLAG_I18N) {
|
||||||
|
try {
|
||||||
|
msg.mdoc.content = JSON.parse(msg.mdoc.content);
|
||||||
|
if (msg.mdoc.content.url) msg.mdoc.url = msg.mdoc.content.url;
|
||||||
|
msg.mdoc.content = i18n(msg.mdoc.content.message, ...msg.mdoc.content.params);
|
||||||
|
} catch (e) {
|
||||||
|
msg.mdoc.content = i18n(msg.mdoc.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (msg.mdoc.flag & FLAG_ALERT) {
|
if (msg.mdoc.flag & FLAG_ALERT) {
|
||||||
// Is alert
|
// Is alert
|
||||||
new InfoDialog({
|
new InfoDialog({
|
||||||
@@ -24,7 +35,7 @@ const onmessage = (msg) => {
|
|||||||
if (msg.mdoc.flag & FLAG_INFO) {
|
if (msg.mdoc.flag & FLAG_INFO) {
|
||||||
if (previous) previous.hide();
|
if (previous) previous.hide();
|
||||||
previous = new VjNotification({
|
previous = new VjNotification({
|
||||||
message: i18n(msg.mdoc.content),
|
message: msg.mdoc.content,
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
});
|
});
|
||||||
previous.show();
|
previous.show();
|
||||||
@@ -33,15 +44,17 @@ const onmessage = (msg) => {
|
|||||||
if (document.hidden) return false;
|
if (document.hidden) return false;
|
||||||
// Is message
|
// Is message
|
||||||
new VjNotification({
|
new VjNotification({
|
||||||
...(msg.udoc._id === 1 && msg.mdoc.flag & FLAG_RICHTEXT)
|
...(msg.udoc._id === 1)
|
||||||
? { message: i18n('You received a system message, click here to view.') }
|
? {
|
||||||
: {
|
type: 'info',
|
||||||
|
message: msg.mdoc.flag & FLAG_RICHTEXT ? i18n('You received a system message, click here to view.') : msg.mdoc.content,
|
||||||
|
} : {
|
||||||
title: msg.udoc.uname,
|
title: msg.udoc.uname,
|
||||||
avatar: msg.udoc.avatarUrl,
|
avatar: msg.udoc.avatarUrl,
|
||||||
message: msg.mdoc.content,
|
message: msg.mdoc.content,
|
||||||
},
|
},
|
||||||
duration: 15000,
|
duration: 15000,
|
||||||
action: () => window.open(`/home/messages?uid=${msg.udoc._id}`, '_blank'),
|
action: () => window.open(msg.mdoc.url ? msg.mdoc.url : `/home/messages?uid=${msg.udoc._id}`, '_blank'),
|
||||||
}).show();
|
}).show();
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ export const FLAG_UNREAD = 1;
|
|||||||
export const FLAG_ALERT = 2;
|
export const FLAG_ALERT = 2;
|
||||||
export const FLAG_RICHTEXT = 4;
|
export const FLAG_RICHTEXT = 4;
|
||||||
export const FLAG_INFO = 8;
|
export const FLAG_INFO = 8;
|
||||||
|
export const FLAG_I18N = 16;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import $ from 'jquery';
|
||||||
|
import { NamedPage } from 'vj/misc/Page';
|
||||||
|
function handleReplyOrBroadcast(ev) {
|
||||||
|
const title = $(ev.currentTarget).data('title');
|
||||||
|
const did = $(ev.currentTarget).data('did');
|
||||||
|
|
||||||
|
$('#reply_or_broadcast .section_title').text(title);
|
||||||
|
$('#reply_or_broadcast [name="did"]').val(did ?? '');
|
||||||
|
const $item = $(`#clarification_${did} .media`);
|
||||||
|
if ($item.length) {
|
||||||
|
$('#reply_or_broadcast .form__item_subject').hide();
|
||||||
|
$('#reply_or_broadcast .clarification-container').empty().append($item.clone());
|
||||||
|
} else {
|
||||||
|
$('#reply_or_broadcast .form__item_subject').show();
|
||||||
|
$('#reply_or_broadcast .clarification-container').empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = new NamedPage('contest_manage', () => {
|
||||||
|
$(document).on('click', '[name="broadcast"]', handleReplyOrBroadcast);
|
||||||
|
$(document).on('click', '[name="reply"]', handleReplyOrBroadcast);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default page;
|
||||||
@@ -6,3 +6,7 @@
|
|||||||
{% macro render_duration(tdoc) %}
|
{% macro render_duration(tdoc) %}
|
||||||
{{ tdoc.duration|round(1) if tdoc.duration else ((tdoc.endAt.getTime() - tdoc.beginAt.getTime()) /1000 / 3600)|round(1) }}
|
{{ tdoc.duration|round(1) if tdoc.duration else ((tdoc.endAt.getTime() - tdoc.beginAt.getTime()) /1000 / 3600)|round(1) }}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro render_clarification_subject(tdoc, pdict, subject) %}
|
||||||
|
{% if subject == 0 %}{{ _('General Issue') }}{% elif subject == -1 %}{{ _('Technical Issue') }}{% else %}{{ String.fromCharCode(65 + tdoc.pids.indexOf(subject)) + 1 }}. {{ pdict[subject].title }}{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
@@ -1,69 +1,143 @@
|
|||||||
{% extends "layout/basic.html" %}
|
{% extends "layout/basic.html" %}
|
||||||
|
{% import "components/contest.html" as contest with context %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="medium-5 columns">
|
<div class="medium-9 columns">
|
||||||
<div class="section">
|
<div class="row">
|
||||||
<div class="section__body no-padding">
|
<div class="medium-7 columns">
|
||||||
<table class="data-table">
|
<div class="section">
|
||||||
<colgroup>
|
<div class="section__body no-padding">
|
||||||
<col class="col--problem">
|
<table class="data-table">
|
||||||
</colgroup>
|
<colgroup>
|
||||||
<thead>
|
<col class="col--problem">
|
||||||
<tr>
|
</colgroup>
|
||||||
<th class="col--problem">{{ _('Problem') }}</th>
|
<thead>
|
||||||
</tr>
|
<tr>
|
||||||
</thead>
|
<th class="col--problem">{{ _('Problem') }}</th>
|
||||||
<tbody>
|
</tr>
|
||||||
{%- for pid in tdoc.pids -%}
|
</thead>
|
||||||
<tr>
|
<tbody>
|
||||||
<td class="col--problem col--problem-name">
|
{%- for pid in tdoc.pids -%}
|
||||||
<a href="{{ url('problem_detail', pid=pid) }}">
|
<tr>
|
||||||
<b>{{ String.fromCharCode(65+loop.index0) }}</b> {{ pdict[pid].title }}
|
<td class="col--problem col--problem-name">
|
||||||
</a>
|
<a href="{{ url('problem_detail', pid=pid) }}">
|
||||||
</td>
|
<b>{{ String.fromCharCode(65+loop.index0) }}</b> {{ pdict[pid].title }}
|
||||||
</tr>
|
</a>
|
||||||
{%- endfor -%}
|
</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
{%- endfor -%}
|
||||||
</div>
|
</tbody>
|
||||||
</div>
|
</table>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
{{ noscript_note.render() }}
|
<div class="medium-5 columns">
|
||||||
{% include "partials/files.html" %}
|
<div class="section">
|
||||||
<div class="section__body">
|
<div class="section__header">
|
||||||
<button class="rounded button" name="remove_selected">{{ _('Remove Selected') }}</button>
|
<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-12 columns">
|
||||||
|
<div class="section">
|
||||||
|
<div class="section__header">
|
||||||
|
<h1 class="section__title">{{ _('Contest Clarifications') }}</h1>
|
||||||
|
<div class="section__tools">
|
||||||
|
<a href="#reply_or_broadcast" class="primary rounded button" name="broadcast" data-title="{{ _('Send Broadcast Message') }}" data-did="">{{ _('Send Broadcast Message') }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section__body">
|
||||||
|
{% if not tcdocs.length %}
|
||||||
|
{{ nothing.render('Oh, there is no clarification!') }}
|
||||||
|
{% else %}
|
||||||
|
<ul class="dczcomments__list">
|
||||||
|
{% for doc in tcdocs %}
|
||||||
|
<li class="dczcomments__item" id="clarification_{{ doc._id }}" data-did="{{ doc._id }}">
|
||||||
|
<div class="media">
|
||||||
|
<div class="media__body top" style="max-width:100%">
|
||||||
|
<div class="clearfix">
|
||||||
|
<div class="supplementary dczcomments__supplementary">
|
||||||
|
{{_('Subject') }}: {{ contest.render_clarification_subject(tdoc,pdict,doc.subject) }} | {% if doc.owner == 0 %}<b>{{ _('Jury') }}</b>{% else %}{{ user.render_inline(udict[doc.owner], badge=false) }}{% endif %} @ {{ datetimeSpan(doc._id)|safe }}
|
||||||
|
</div>
|
||||||
|
{% if doc.owner %}
|
||||||
|
<div class="dczcomments__operations nojs--hide">
|
||||||
|
<a href="#reply_or_broadcast" name="reply" data-title="{{ _('Reply') }}#{{ doc._id }}" data-did="{{ doc._id }}"
|
||||||
|
><span class="icon icon-reply"></span>{{ _('Reply') }}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="typo" data-emoji-enabled>
|
||||||
|
{{ doc['content']|markdown|safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="dczcomments__replies">
|
||||||
|
{%- for rdoc in doc['reply'] -%}
|
||||||
|
<li class="dczcomments__reply">
|
||||||
|
<div class="media">
|
||||||
|
<div class="media__left top"> </div>
|
||||||
|
<div class="media__body top">
|
||||||
|
<div class="clearfix">
|
||||||
|
<div class="supplementary dczcomments__supplementary">
|
||||||
|
<b>{{ _('Jury') }}</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="typo" data-emoji-enabled>
|
||||||
|
{{ rdoc['content']|markdown|safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{%- endfor -%}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div id="reply_or_broadcast">
|
||||||
|
<div class="section__header">
|
||||||
|
<h1 class="section__title">{{ _('Send Broadcast Message') }}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="section__body typo">
|
||||||
|
<form method="post">
|
||||||
|
<div class="clarification-container"></div>
|
||||||
|
<input type="hidden" name="operation" value="clarification">
|
||||||
|
<input type="hidden" name="did" value="">
|
||||||
|
<div class="row">
|
||||||
|
<div class="medium-12 columns form__item_subject">
|
||||||
|
<label>
|
||||||
|
{{ _('Subject') }}
|
||||||
|
<select name="subject" class="select">
|
||||||
|
<option value="0">{{ _('General Issue') }}</option>
|
||||||
|
<option value="-1">{{ _('Technical Issue') }}</option>
|
||||||
|
{% for pid in tdoc.pids %}
|
||||||
|
<option value="{{ pid }}">{{ String.fromCharCode(65+loop.index0) }}. {{ pdict[pid].title }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="medium-12 columns form__item">
|
||||||
|
<label>
|
||||||
|
{{ _('Content') }}
|
||||||
|
<textarea name="content" class="textbox" data-markdown required></textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="rounded primary button">{{ _('Submit') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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 name="operation" value="broadcast" type="submit" class="rounded primary button">
|
|
||||||
{{ _('Send') }}
|
|
||||||
</button>
|
|
||||||
</div></div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="medium-3 columns">
|
<div class="medium-3 columns">
|
||||||
{% include 'partials/contest_sidebar_management.html' %}
|
{% include 'partials/contest_sidebar_management.html' %}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
{{ set(UiContext, 'tdoc', tdoc) }}
|
{{ set(UiContext, 'tdoc', tdoc) }}
|
||||||
{{ set(UiContext, 'tsdoc', tsdoc) }}
|
{{ set(UiContext, 'tsdoc', tsdoc) }}
|
||||||
<div class="row">
|
<div class="row" data-sticky-parent>
|
||||||
<div class="medium-9 columns">
|
<div class="medium-9 columns">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section__header">
|
<div class="section__header">
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="section__body no-padding">
|
<div class="section__body no-padding">
|
||||||
{% if not canViewRecord %}
|
{% if not canViewRecord %}
|
||||||
{{ nothing.render('According to the contest rules, you cannot view your submission details at current.') }}'
|
{{ nothing.render('According to the contest rules, you cannot view your submission details at current.') }}
|
||||||
{% elif not rdocs.length %}
|
{% elif not rdocs.length %}
|
||||||
{{ nothing.render('Oh, there is no submission!') }}
|
{{ nothing.render('Oh, there is no submission!') }}
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -99,8 +99,86 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<div class="section__header">
|
||||||
|
<h1 class="section__title">{{ _('Contest Clarifications') }}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="section__body">
|
||||||
|
{% if not tcdocs.length %}
|
||||||
|
{{ nothing.render('Oh, there is no clarification!') }}
|
||||||
|
{% else %}
|
||||||
|
<ul class="dczcomments__list">
|
||||||
|
{% for doc in tcdocs %}
|
||||||
|
<li class="dczcomments__item">
|
||||||
|
<div class="media">
|
||||||
|
<div class="media__body top">
|
||||||
|
<div class="clearfix">
|
||||||
|
<div class="supplementary dczcomments__supplementary">
|
||||||
|
{{_('Subject') }}: {{ contest.render_clarification_subject(tdoc,pdict,doc.subject) }} | {% if doc.owner == 0 %}<b>{{ _('Jury') }}</b>{% else %}{{ user.render_inline(udict[doc.owner], badge=false) }}{% endif %} @ {{ datetimeSpan(doc._id)|safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="typo" data-emoji-enabled>
|
||||||
|
{{ doc['content']|markdown|safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="dczcomments__replies">
|
||||||
|
{%- for rdoc in doc['reply'] -%}
|
||||||
|
<li class="dczcomments__reply">
|
||||||
|
<div class="media">
|
||||||
|
<div class="media__left top"> </div>
|
||||||
|
<div class="media__body top">
|
||||||
|
<div class="clearfix">
|
||||||
|
<div class="supplementary dczcomments__supplementary">
|
||||||
|
<b>{{ _('Jury') }}</b> @ {{ datetimeSpan(rdoc['_id'])|safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="typo" data-emoji-enabled>
|
||||||
|
{{ rdoc['content']|markdown|safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{%- endfor -%}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if tsdoc.attend %}
|
||||||
|
<div class="section__header">
|
||||||
|
<h1 class="section__title">{{ _('Send Clarification Request') }}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="section__body typo">
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="operation" value="clarification">
|
||||||
|
<div class="row">
|
||||||
|
<div class="medium-12 columns form__item">
|
||||||
|
<label>
|
||||||
|
{{ _('Subject') }}
|
||||||
|
<select name="subject" class="select">
|
||||||
|
<option value="0">{{ _('General Issue') }}</option>
|
||||||
|
<option value="-1">{{ _('Technical Issue') }}</option>
|
||||||
|
{% for pid in tdoc.pids %}
|
||||||
|
<option value="{{ pid }}">{{ String.fromCharCode(65+loop.index0) }}. {{ pdict[pid].title }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="medium-12 columns form__item">
|
||||||
|
<label>
|
||||||
|
{{ _('Content') }}
|
||||||
|
<textarea name="content" class="textbox" data-markdown required></textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="rounded primary button">{{ _('Submit') }}</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="medium-3 columns">
|
<div class="medium-3 columns" data-sticky="large">
|
||||||
{% set owner_udoc = udict[tdoc.owner] %}
|
{% set owner_udoc = udict[tdoc.owner] %}
|
||||||
{% include "partials/contest_sidebar.html" %}
|
{% include "partials/contest_sidebar.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user