mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 17:36:08 +08:00
core&ui: contest enhancement (#472)
Co-authored-by: panda <panda_dtdyy@outlook.com>
This commit is contained in:
55 files changed
+965
-282
No files matched your search
@@ -40,6 +40,26 @@ Telegram [@webpack_exports_undefined](https://t.me/webpack_exports_undefined)
|
||||
<details>
|
||||
<summary><h2>更新日志(点击展开)</h2></summary>
|
||||
|
||||
### Hydro 4.5.0 / UI 4.44.0
|
||||
- fps: 修复题目中含空文件导致导入失败的问题
|
||||
- core: 封禁用户时支持附加理由
|
||||
- vjudge: codeforces: 跳过无法访问的 1769 和 1772 比赛
|
||||
- ui: 收藏题目操作不再触发页面刷新
|
||||
- core: 重测时检查题目配置文件有效性
|
||||
- core: 退出时自动清理临时文件
|
||||
- core: 禁止使用 . 作为文件名
|
||||
- import-qduoj: 跳过不合法的题目
|
||||
- core: 修复提交答案题的比赛代码导出
|
||||
- judge: 添加 stdioLimit 项
|
||||
- ui: 修复 message.FLAG_ALERT 显示
|
||||
- core: training 可上传文件
|
||||
- ui: 优化比赛导航栏
|
||||
- ui: 比赛成绩表支持关注队伍
|
||||
- core: 允许克隆比赛/作业
|
||||
- ui: 比赛编辑页面添加功能入口
|
||||
- core: 支持打星参赛
|
||||
- core: 整题重测时跳过已取消成绩的提交
|
||||
|
||||
### Hydro 4.4.5 / UI 4.43.0
|
||||
- core: 修复比赛基于 ID 搜索题目的功能
|
||||
- judge: 修复 testlib 错误信息显示异常的问题
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/fps-importer",
|
||||
"version": "1.5.2",
|
||||
"version": "1.5.3",
|
||||
"description": "Import FPS problems",
|
||||
"main": "index.ts",
|
||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@hydrooj/hydrojudge",
|
||||
"bin": "bin/hydrojudge.js",
|
||||
"version": "3.1.12",
|
||||
"version": "3.1.13",
|
||||
"main": "package.json",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||
|
||||
@@ -8,7 +8,9 @@ contest_detail: Contest Detail
|
||||
contest_edit: Contest Edit
|
||||
contest_files: Contest Files
|
||||
contest_main: Contest
|
||||
contest_problemlist: Problem List
|
||||
contest_scoreboard: Scoreboard
|
||||
contest_user: Attendee Manage
|
||||
discussion_create: Create Discussion
|
||||
discussion_detail: Discussion Detail
|
||||
discussion_edit: Discussion Edit
|
||||
|
||||
@@ -294,8 +294,7 @@ Evaluated difficulty: 평가된 난이도
|
||||
expand: 확장
|
||||
Expire: 만료
|
||||
Export All Code: 모든 코드 내보내기
|
||||
Export as CSV: CSV로 내보내기
|
||||
Export as HTML: HTML로 내보내기
|
||||
Export as {0}: '{0}로 내보내기'
|
||||
Extension (days): 추가 (날짜)
|
||||
Extension Score Penalty: 추가 패널티
|
||||
Failed to join the domain. You are already a member.: 그룹 참여 실패. 이미 참여함.
|
||||
|
||||
@@ -88,6 +88,7 @@ Attended: 已参加
|
||||
author: 作者
|
||||
Auto format code: 自动格式化代码
|
||||
Avatar: 头像
|
||||
Back to contest list: 返回比赛列表
|
||||
Be Copied: 被复制
|
||||
Begin at: 开始于
|
||||
Begin Date: 开始日期
|
||||
@@ -159,7 +160,9 @@ contest_detail: 比赛详情
|
||||
contest_edit: 编辑比赛
|
||||
contest_files: 比赛文件
|
||||
contest_main: 比赛
|
||||
contest_problemlist: 题目列表
|
||||
contest_scoreboard: 比赛成绩表
|
||||
contest_user: 参赛者管理
|
||||
Contest: 比赛
|
||||
Contests per page: 每页展示的比赛数量
|
||||
Continue: 继续
|
||||
@@ -322,8 +325,7 @@ Evaluated difficulty: 估计的难度
|
||||
expand: 展开
|
||||
Expire: 过期
|
||||
Export All Code: 导出所有代码
|
||||
Export as CSV: 导出为 CSV
|
||||
Export as HTML: 导出为 HTML
|
||||
Export as {0}: 导出为 {0}
|
||||
Extension (days): 最长延期 (日)
|
||||
Extension Score Penalty: 延期递交扣分规则
|
||||
Failed to join the domain. You are already a member.: 加入域失败,您已是该域的成员。
|
||||
@@ -437,8 +439,8 @@ List View: 列表视图
|
||||
Live: 正在进行
|
||||
Live...: 正在进行…
|
||||
Location: 位置
|
||||
Lock Topic: 锁定主题
|
||||
Lock discussions: 锁定讨论
|
||||
Lock Topic: 锁定主题
|
||||
Login to Attend Contest: 登录后参加比赛
|
||||
Login to Claim Homework: 登录后认领作业
|
||||
Login to Create a Discussion: 登录后创建讨论
|
||||
@@ -868,8 +870,8 @@ View or create discussion: 查看与发表讨论
|
||||
View or submit solution: 查看与发表题解
|
||||
View Owned Problems: 查看自己的题目
|
||||
View Owned Training Plans: 查看自己的训练计划
|
||||
View problem solutions: 查看题解
|
||||
View problem solutions after accept: 题目通过后查看题解
|
||||
View problem solutions: 查看题解
|
||||
View Problem: 查看题目
|
||||
View problems: 查看题目
|
||||
View ranking: 查看排名
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydrooj",
|
||||
"version": "4.4.5",
|
||||
"version": "4.5.0",
|
||||
"bin": "bin/hydrooj.js",
|
||||
"main": "src/plugin-api",
|
||||
"module": "src/plugin-api",
|
||||
|
||||
@@ -7,9 +7,9 @@ import {
|
||||
Counter, sortFiles, streamToBuffer, Time,
|
||||
} from '@hydrooj/utils/lib/utils';
|
||||
import {
|
||||
BadRequestError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError,
|
||||
ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError, InvalidTokenError,
|
||||
NotAssignedError, PermissionError, ValidationError,
|
||||
BadRequestError, ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError,
|
||||
ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError, ForbiddenError,
|
||||
InvalidTokenError, NotAssignedError, PermissionError, ValidationError,
|
||||
} from '../error';
|
||||
import { Tdoc } from '../interface';
|
||||
import paginate from '../lib/paginate';
|
||||
@@ -112,32 +112,33 @@ export class ContestDetailBaseHandler extends Handler {
|
||||
|
||||
@param('tid', Types.ObjectID, true)
|
||||
async after(domainId: string, tid: ObjectID) {
|
||||
if (!tid || this.tdoc.rule === 'homework' || !contest.isOngoing(this.tdoc, this.tsdoc)) return;
|
||||
if (!tid || this.tdoc.rule === 'homework') return;
|
||||
if (this.request.json || !this.response.template) return;
|
||||
const pdoc = 'pdoc' in this ? (this as any).pdoc : {};
|
||||
this.response.body.overrideNav = [
|
||||
{ name: 'homepage', args: { prefix: 'homepage' }, checker: () => true },
|
||||
{
|
||||
name: 'contest_main',
|
||||
args: {},
|
||||
displayName: 'Back to contest list',
|
||||
checker: () => true,
|
||||
},
|
||||
{
|
||||
name: 'contest_detail',
|
||||
displayName: this.tdoc.title,
|
||||
args: { tid, prefix: 'contest_detail' },
|
||||
checker: () => true,
|
||||
},
|
||||
{
|
||||
name: 'contest_problemlist',
|
||||
args: { tid, prefix: 'contest_problemlist' },
|
||||
checker: () => contest.isDone(this.tdoc, this.tsdoc)
|
||||
|| (contest.isOngoing(this.tdoc, this.tsdoc) && this.tsdoc?.attend),
|
||||
},
|
||||
{
|
||||
name: 'contest_scoreboard',
|
||||
args: { tid, prefix: 'contest_scoreboard' },
|
||||
checker: () => contest.canShowScoreboard.call(this, this.tdoc, true),
|
||||
},
|
||||
{
|
||||
name: 'record_main',
|
||||
args: {
|
||||
prefix: 'record',
|
||||
query: contest.canShowRecord.call(this, this.tdoc, true)
|
||||
? { tid }
|
||||
: { tid, uidOrName: this.user._id },
|
||||
},
|
||||
checker: () => contest.canShowSelfRecord.call(this, this.tdoc, true),
|
||||
},
|
||||
{
|
||||
displayName: `${String.fromCharCode(65 + this.tdoc.pids.indexOf(pdoc.docId))}. ${pdoc.title}`,
|
||||
args: { query: { tid }, pid: pdoc.docId, prefix: 'contest_detail_problem' },
|
||||
@@ -147,10 +148,26 @@ export class ContestDetailBaseHandler extends Handler {
|
||||
}
|
||||
}
|
||||
|
||||
export class ContestDetailHandler extends ContestDetailBaseHandler {
|
||||
export class ContestDetailHandler extends Handler {
|
||||
tdoc?: Tdoc<30>;
|
||||
tsdoc?: any;
|
||||
|
||||
@param('tid', Types.ObjectID)
|
||||
async prepare(domainId: string, tid: ObjectID) {
|
||||
[this.tdoc, this.tsdoc] = await Promise.all([
|
||||
contest.get(domainId, tid),
|
||||
contest.getStatus(domainId, tid, this.user._id),
|
||||
]);
|
||||
if (contest.RULES[this.tdoc.rule].hidden) throw new ContestNotFoundError(domainId, tid);
|
||||
if (this.tdoc.assign?.length && !this.user.own(this.tdoc)) {
|
||||
const groups = await user.listGroup(domainId, this.user._id);
|
||||
if (!Set.intersection(this.tdoc.assign, groups.map((i) => i.name)).size) {
|
||||
throw new NotAssignedError('contest', tid);
|
||||
}
|
||||
}
|
||||
if (this.tdoc.duration && this.tsdoc?.startAt) {
|
||||
this.tsdoc.endAt = moment(this.tsdoc.startAt).add(this.tdoc.duration, 'hours').toDate();
|
||||
}
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectID)
|
||||
@@ -160,32 +177,41 @@ export class ContestDetailHandler extends ContestDetailBaseHandler {
|
||||
this.response.body = {
|
||||
tdoc: this.tdoc, tsdoc: pick(this.tsdoc, ['attend', 'startAt']), udict,
|
||||
};
|
||||
if (!this.request.json) {
|
||||
this.response.body.tdoc.content = this.response.body.tdoc.content
|
||||
.replace(/\(file:\/\//g, `(./${this.tdoc.docId}/file/`)
|
||||
.replace(/="file:\/\//g, `="./${this.tdoc.docId}/file/`);
|
||||
}
|
||||
if (
|
||||
(contest.isNotStarted(this.tdoc) || (!this.tsdoc?.attend && !contest.isDone(this.tdoc)))
|
||||
&& !this.user.own(this.tdoc)
|
||||
&& !this.user.hasPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD)
|
||||
) return;
|
||||
const pdict = await problem.getList(domainId, this.tdoc.pids, true, true, problem.PROJECTION_CONTEST_LIST);
|
||||
let psdict = {};
|
||||
let rdict = {};
|
||||
if (this.tsdoc) {
|
||||
if (this.tsdoc.attend && !this.tsdoc.startAt && contest.isOngoing(this.tdoc)) {
|
||||
await contest.setStatus(domainId, tid, this.user._id, { startAt: new Date() });
|
||||
this.tsdoc.startAt = new Date();
|
||||
}
|
||||
psdict = this.tsdoc.detail || {};
|
||||
if (contest.canShowSelfRecord.call(this, this.tdoc)) {
|
||||
rdict = await record.getList(domainId, Object.values(psdict).map((i: any) => i.rid));
|
||||
} else {
|
||||
for (const i in psdict) rdict[psdict[i].rid] = { _id: psdict[i].rid };
|
||||
}
|
||||
}
|
||||
Object.assign(this.response.body, { pdict, psdict, rdict });
|
||||
if (this.request.json) return;
|
||||
this.response.body.tdoc.content = this.response.body.tdoc.content
|
||||
.replace(/\(file:\/\//g, `(./${this.tdoc.docId}/file/`)
|
||||
.replace(/="file:\/\//g, `="./${this.tdoc.docId}/file/`);
|
||||
const pdoc = 'pdoc' in this ? (this as any).pdoc : {};
|
||||
this.response.body.overrideNav = [
|
||||
{
|
||||
name: 'contest_main',
|
||||
args: {},
|
||||
displayName: 'Back to contest list',
|
||||
checker: () => true,
|
||||
},
|
||||
{
|
||||
name: 'contest_detail',
|
||||
displayName: this.tdoc.title,
|
||||
args: { tid, prefix: 'contest_detail' },
|
||||
checker: () => true,
|
||||
},
|
||||
{
|
||||
name: 'contest_problemlist',
|
||||
args: { tid },
|
||||
checker: () => contest.isDone(this.tdoc, this.tsdoc)
|
||||
|| (contest.isOngoing(this.tdoc, this.tsdoc) && this.tsdoc?.attend),
|
||||
},
|
||||
{
|
||||
name: 'contest_scoreboard',
|
||||
args: { tid, prefix: 'contest_scoreboard' },
|
||||
checker: () => contest.canShowScoreboard.call(this, this.tdoc, true),
|
||||
},
|
||||
{
|
||||
displayName: `${String.fromCharCode(65 + this.tdoc.pids.indexOf(pdoc.docId))}. ${pdoc.title}`,
|
||||
args: { query: { tid }, pid: pdoc.docId, prefix: 'contest_detail_problem' },
|
||||
checker: () => 'pdoc' in this,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectID)
|
||||
@@ -196,16 +222,36 @@ export class ContestDetailHandler extends ContestDetailBaseHandler {
|
||||
await contest.attend(domainId, tid, this.user._id);
|
||||
this.back();
|
||||
}
|
||||
}
|
||||
|
||||
export class ContestProblemListHandler extends ContestDetailBaseHandler {
|
||||
@param('tid', Types.ObjectID)
|
||||
async postDelete(domainId: string, tid: ObjectID) {
|
||||
if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST);
|
||||
await contest.del(domainId, tid);
|
||||
await record.updateMulti(domainId, { domainId, contest: tid }, undefined, undefined, { contest: '' });
|
||||
await ScheduleModel.deleteMany({
|
||||
type: 'schedule', subType: 'contest', domainId, tid,
|
||||
});
|
||||
this.response.redirect = this.url('contest_main');
|
||||
async get(domainId: string, tid: ObjectID) {
|
||||
if (contest.isNotStarted(this.tdoc) || (!this.tsdoc?.attend && !contest.isDone(this.tdoc))) throw new ForbiddenError();
|
||||
const [pdict, udict] = await Promise.all([
|
||||
problem.getList(domainId, this.tdoc.pids, true, true, problem.PROJECTION_CONTEST_LIST),
|
||||
user.getList(domainId, [this.tdoc.owner, this.user._id]),
|
||||
]);
|
||||
this.response.body = {
|
||||
pdict, psdict: {}, udict, rdict: {}, tdoc: this.tdoc, tsdoc: this.tsdoc,
|
||||
};
|
||||
this.response.template = 'contest_problemlist.html';
|
||||
if (!this.tsdoc) return;
|
||||
if (this.tsdoc.attend && !this.tsdoc.startAt && contest.isOngoing(this.tdoc)) {
|
||||
await contest.setStatus(domainId, tid, this.user._id, { startAt: new Date() });
|
||||
this.tsdoc.startAt = new Date();
|
||||
}
|
||||
this.response.body.psdict = this.tsdoc.detail || {};
|
||||
const psdocs: any[] = Object.values(this.response.body.psdict);
|
||||
if (contest.canShowSelfRecord.call(this, this.tdoc)) {
|
||||
[this.response.body.rdict, this.response.body.rdocs] = await Promise.all([
|
||||
record.getList(domainId, psdocs.map((i: any) => i.rid)),
|
||||
await record.getMulti(domainId, { tid, uid: this.user._id }).sort({ _id: -1 }).toArray(),
|
||||
]);
|
||||
this.response.body.canViewRecord = true;
|
||||
} else {
|
||||
for (const i of psdocs) this.response.body.rdict[i.rid] = { _id: i.rid };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,11 +406,13 @@ export class ContestEditHandler extends Handler {
|
||||
@param('assign', Types.CommaSeperatedArray, true)
|
||||
@param('lock', Types.UnsignedInt, true)
|
||||
@param('contestDuration', Types.Float, true)
|
||||
async post(
|
||||
@param('maintainer', Types.NumericArray, true)
|
||||
@param('allowViewCode', Types.Boolean, true)
|
||||
async postUpdate(
|
||||
domainId: string, tid: ObjectID, beginAtDate: string, beginAtTime: string, duration: number,
|
||||
title: string, content: string, rule: string, _pids: string, rated = false,
|
||||
_code = '', autoHide = false, assign: string[] = null, lock: number = null,
|
||||
contestDuration: number = null,
|
||||
contestDuration: number = null, maintainer: number[] = null, allowViewCode = true,
|
||||
) {
|
||||
if (autoHide) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
|
||||
const pids = _pids.replace(/,/g, ',').split(',').map((i) => +i).filter((i) => i);
|
||||
@@ -405,11 +453,22 @@ export class ContestEditHandler extends Handler {
|
||||
});
|
||||
}
|
||||
await contest.edit(domainId, tid, {
|
||||
assign, _code, autoHide, lockAt,
|
||||
assign, _code, autoHide, lockAt, maintainer, allowViewCode,
|
||||
});
|
||||
this.response.body = { tid };
|
||||
this.response.redirect = this.url('contest_detail', { tid });
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectID)
|
||||
async postDelete(domainId: string, tid: ObjectID) {
|
||||
if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST);
|
||||
await contest.del(domainId, tid);
|
||||
await record.updateMulti(domainId, { domainId, contest: tid }, undefined, undefined, { contest: '' });
|
||||
await ScheduleModel.deleteMany({
|
||||
type: 'schedule', subType: 'contest', domainId, tid,
|
||||
});
|
||||
this.response.redirect = this.url('contest_main');
|
||||
}
|
||||
}
|
||||
|
||||
export class ContestCodeHandler extends Handler {
|
||||
@@ -498,7 +557,7 @@ export class ContestFilesHandler extends ContestDetailBaseHandler {
|
||||
if (filename.includes('/') || filename.includes('..')) throw new ValidationError('filename', null, 'Bad filename');
|
||||
await storage.put(`contest/${domainId}/${tid}/${filename}`, file.filepath, this.user._id);
|
||||
const meta = await storage.getMeta(`contest/${domainId}/${tid}/${filename}`);
|
||||
const payload = { name: filename, ...pick(meta, ['size', 'lastModified', 'etag']) };
|
||||
const payload = { _id: filename, name: filename, ...pick(meta, ['size', 'lastModified', 'etag']) };
|
||||
if (!meta) throw new FileUploadError();
|
||||
await contest.edit(domainId, tid, { files: [...(this.tdoc.files || []), payload] });
|
||||
this.back();
|
||||
@@ -533,10 +592,45 @@ export class ContestFileDownloadHandler extends ContestDetailBaseHandler {
|
||||
}
|
||||
}
|
||||
|
||||
export class ContestUserHandler extends ContestDetailBaseHandler {
|
||||
@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;
|
||||
});
|
||||
const udict = await user.getListForRender(domainId, [this.tdoc.owner, ...tsdocs.map((i) => i.uid)]);
|
||||
this.response.body = { tdoc: this.tdoc, tsdocs, udict };
|
||||
this.response.pjax = 'partials/contest_user.html';
|
||||
this.response.template = 'contest_user.html';
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectID)
|
||||
@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();
|
||||
}
|
||||
|
||||
@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);
|
||||
@@ -544,4 +638,5 @@ export async function apply(ctx) {
|
||||
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);
|
||||
}
|
||||
@@ -442,6 +442,7 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler {
|
||||
const rdocs = await record.getMulti(domainId, {
|
||||
pid,
|
||||
contest: { $ne: new ObjectID('0'.repeat(24)) },
|
||||
status: { $ne: STATUS.STATUS_CANCELED },
|
||||
'files.hack': { $exists: false },
|
||||
}).project({ _id: 1, contest: 1 }).toArray();
|
||||
if (!this.pdoc.config || typeof this.pdoc.config === 'string') throw new ProblemConfigError();
|
||||
|
||||
@@ -216,7 +216,10 @@ class RecordDetailHandler extends ContestDetailBaseHandler {
|
||||
record.update(domainId, rid, $set),
|
||||
TaskModel.deleteMany({ rid: this.rdoc._id }),
|
||||
]);
|
||||
if (latest) await postJudge(latest);
|
||||
if (latest) {
|
||||
this.ctx.broadcast('record/change', latest);
|
||||
await postJudge(latest);
|
||||
}
|
||||
this.back();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import assert from 'assert';
|
||||
import { statSync } from 'fs-extra';
|
||||
import { pick } from 'lodash';
|
||||
import { FilterQuery, ObjectID } from 'mongodb';
|
||||
import { ProblemNotFoundError, ValidationError } from '../error';
|
||||
import { sortFiles } from '@hydrooj/utils/lib/utils';
|
||||
import {
|
||||
FileLimitExceededError, FileUploadError, ProblemNotFoundError, ValidationError,
|
||||
} from '../error';
|
||||
import { Tdoc, TrainingDoc } from '../interface';
|
||||
import paginate from '../lib/paginate';
|
||||
import { PERM, PRIV, STATUS } from '../model/builtin';
|
||||
import * as oplog from '../model/oplog';
|
||||
import problem from '../model/problem';
|
||||
import storage from '../model/storage';
|
||||
import * as system from '../model/system';
|
||||
import * as training from '../model/training';
|
||||
import user from '../model/user';
|
||||
import * as bus from '../service/bus';
|
||||
import { Handler, param, Types } from '../service/server';
|
||||
import {
|
||||
Handler, param, post, Types,
|
||||
} from '../service/server';
|
||||
|
||||
async function _parseDagJson(domainId: string, _dag: string): Promise<Tdoc['dag']> {
|
||||
const parsed = [];
|
||||
@@ -149,6 +158,9 @@ class TrainingDetailHandler extends Handler {
|
||||
this.response.body = {
|
||||
tdoc, tsdoc, pids, pdict, psdict, ndict, nsdict, udoc, udict, selfPsdict,
|
||||
};
|
||||
this.response.body.tdoc.description = this.response.body.tdoc.description
|
||||
.replace(/\(file:\/\//g, `(./${tdoc.docId}/file/`)
|
||||
.replace(/="file:\/\//g, `="./${tdoc.docId}/file/`);
|
||||
this.response.pjax = 'partials/training_detail.html';
|
||||
this.response.template = 'training_detail.html';
|
||||
}
|
||||
@@ -219,9 +231,86 @@ class TrainingEditHandler extends Handler {
|
||||
}
|
||||
}
|
||||
|
||||
export class TrainingFilesHandler extends Handler {
|
||||
tdoc: TrainingDoc;
|
||||
|
||||
@param('tid', Types.ObjectID)
|
||||
async prepare(domainId: string, tid: ObjectID) {
|
||||
this.tdoc = await training.get(domainId, tid);
|
||||
if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_TRAINING);
|
||||
else this.checkPerm(PERM.PERM_EDIT_TRAINING_SELF);
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectID)
|
||||
async get(domainId: string, tid: ObjectID) {
|
||||
if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_TRAINING);
|
||||
this.response.body = {
|
||||
tdoc: this.tdoc,
|
||||
tsdoc: await training.getStatus(domainId, this.tdoc.docId, this.user._id),
|
||||
udoc: await user.getById(domainId, this.tdoc.owner),
|
||||
files: sortFiles(this.tdoc.files || []),
|
||||
urlForFile: (filename: string) => this.url('training_file_download', { tid, filename }),
|
||||
};
|
||||
this.response.pjax = 'partials/files.html';
|
||||
this.response.template = 'training_files.html';
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectID)
|
||||
@post('filename', Types.Name, true)
|
||||
async postUploadFile(domainId: string, tid: ObjectID, filename: string) {
|
||||
if ((this.tdoc.files?.length || 0) >= system.get('limit.contest_files')) {
|
||||
throw new FileLimitExceededError('count');
|
||||
}
|
||||
const file = this.request.files?.file;
|
||||
if (!file) throw new ValidationError('file');
|
||||
const f = statSync(file.filepath);
|
||||
const size = Math.sum((this.tdoc.files || []).map((i) => i.size)) + f.size;
|
||||
if (size >= system.get('limit.contest_files_size')) {
|
||||
throw new FileLimitExceededError('size');
|
||||
}
|
||||
if (!filename) filename = file.originalFilename || String.random(16);
|
||||
if (filename.includes('/') || filename.includes('..')) throw new ValidationError('filename', null, 'Bad filename');
|
||||
await storage.put(`training/${domainId}/${tid}/${filename}`, file.filepath, this.user._id);
|
||||
const meta = await storage.getMeta(`training/${domainId}/${tid}/${filename}`);
|
||||
const payload = { _id: filename, name: filename, ...pick(meta, ['size', 'lastModified', 'etag']) };
|
||||
if (!meta) throw new FileUploadError();
|
||||
await training.edit(domainId, tid, { files: [...(this.tdoc.files || []), payload] });
|
||||
this.back();
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectID)
|
||||
@post('files', Types.Array)
|
||||
async postDeleteFiles(domainId: string, tid: ObjectID, files: string[]) {
|
||||
await Promise.all([
|
||||
storage.del(files.map((t) => `contest/${domainId}/${tid}/${t}`), this.user._id),
|
||||
training.edit(domainId, tid, { files: this.tdoc.files.filter((i) => !files.includes(i.name)) }),
|
||||
]);
|
||||
this.back();
|
||||
}
|
||||
}
|
||||
export class TrainingFileDownloadHandler extends Handler {
|
||||
@param('tid', Types.ObjectID)
|
||||
@param('filename', Types.Name)
|
||||
@param('noDisposition', Types.Boolean)
|
||||
async get(domainId: string, tid: ObjectID, filename: string, noDisposition = false) {
|
||||
this.response.addHeader('Cache-Control', 'public');
|
||||
const target = `training/${domainId}/${tid}/${filename}`;
|
||||
const file = await storage.getMeta(target);
|
||||
await oplog.log(this, 'download.file.training', {
|
||||
target,
|
||||
size: file?.size || 0,
|
||||
});
|
||||
this.response.redirect = await storage.signDownloadLink(
|
||||
target, noDisposition ? undefined : filename, false, 'user',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function apply(ctx) {
|
||||
ctx.Route('training_main', '/training', TrainingMainHandler, PERM.PERM_VIEW_TRAINING);
|
||||
ctx.Route('training_create', '/training/create', TrainingEditHandler);
|
||||
ctx.Route('training_detail', '/training/:tid', TrainingDetailHandler, PERM.PERM_VIEW_TRAINING);
|
||||
ctx.Route('training_edit', '/training/:tid/edit', TrainingEditHandler);
|
||||
ctx.Route('training_files', '/training/:tid/file', TrainingFilesHandler, PERM.PERM_VIEW_TRAINING);
|
||||
ctx.Route('training_file_download', '/training/:tid/file/:filename', TrainingFileDownloadHandler, PERM.PERM_VIEW_TRAINING);
|
||||
}
|
||||
@@ -366,6 +366,8 @@ export interface Tdoc<docType = document['TYPE_CONTEST'] | document['TYPE_TRAINI
|
||||
// For contest
|
||||
lockAt?: Date;
|
||||
unlocked?: boolean;
|
||||
autoHide?: boolean;
|
||||
|
||||
/**
|
||||
* In hours
|
||||
* 在比赛有效时间内选择特定的 X 小时参加比赛(从首次打开比赛算起)
|
||||
@@ -493,6 +495,7 @@ export interface OplogDoc extends Record<string, any> {
|
||||
|
||||
export interface ContestStat extends Record<string, any> {
|
||||
detail: Record<number, Record<string, any>>,
|
||||
unrank?: boolean,
|
||||
}
|
||||
|
||||
export interface ContestRule<T = any> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Cursor } from 'mongodb';
|
||||
|
||||
type ranked = <T>(cursor: Cursor<T>, equ: ((a: T, b: T) => boolean)) => Promise<[number, T][]>;
|
||||
type ranked = <T extends Record<string, any>>(cursor: Cursor<T>, equ: ((a: T, b: T) => boolean)) => Promise<[number, T][]>;
|
||||
|
||||
const ranked: ranked = async (cursor, equ) => {
|
||||
let last = null;
|
||||
@@ -9,6 +9,10 @@ const ranked: ranked = async (cursor, equ) => {
|
||||
const results = [];
|
||||
const docs = await cursor.toArray();
|
||||
for (const doc of docs) {
|
||||
if (doc.unrank) {
|
||||
results.push([0, doc]);
|
||||
continue;
|
||||
}
|
||||
count++;
|
||||
if (!last || !equ(last, doc)) r = count;
|
||||
last = doc;
|
||||
|
||||
@@ -533,7 +533,7 @@ export async function add(
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function edit(domainId: string, tid: ObjectID, $set: any) {
|
||||
export async function edit(domainId: string, tid: ObjectID, $set: Partial<Tdoc>) {
|
||||
if ($set.rule && !RULES[$set.rule]) throw new ValidationError('rule');
|
||||
const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid);
|
||||
if (!tdoc) throw new ContestNotFoundError(domainId, tid);
|
||||
@@ -593,9 +593,9 @@ export async function getListStatus(domainId: string, uid: number, tids: ObjectI
|
||||
return r;
|
||||
}
|
||||
|
||||
export async function attend(domainId: string, tid: ObjectID, uid: number) {
|
||||
export async function attend(domainId: string, tid: ObjectID, uid: number, payload: any = {}) {
|
||||
try {
|
||||
await document.cappedIncStatus(domainId, document.TYPE_CONTEST, tid, uid, 'attend', 1, 0, 1);
|
||||
await document.cappedIncStatus(domainId, document.TYPE_CONTEST, tid, uid, 'attend', 1, 0, 1, payload);
|
||||
} catch (e) {
|
||||
throw new ContestAlreadyAttendedError(tid, uid);
|
||||
}
|
||||
|
||||
@@ -350,12 +350,15 @@ export async function setStatusIfNotCondition<T extends keyof DocStatusType>(
|
||||
export async function cappedIncStatus<T extends keyof DocStatusType>(
|
||||
domainId: string, docType: T, docId: DocStatusType[T]['docId'], uid: number,
|
||||
key: NumberKeys<DocStatusType[T]>, value: number, minValue = -1, maxValue = 1,
|
||||
setPayload: Partial<DocStatusType[T]> = {},
|
||||
): Promise<DocStatusType[T]> {
|
||||
assert(value !== 0);
|
||||
const $not = value > 0 ? { $gte: maxValue } : { $lte: minValue };
|
||||
const operation = { $inc: { [key]: value } } as UpdateQuery<DocStatusType[T]>;
|
||||
if (Object.keys(setPayload).length) operation.$set = setPayload;
|
||||
const res = await collStatus.findOneAndUpdate(
|
||||
{ domainId, docType, docId, uid, [key]: { $not } },
|
||||
{ $inc: { [key]: value } },
|
||||
operation,
|
||||
{ upsert: true, returnDocument: 'after' },
|
||||
);
|
||||
return res.value;
|
||||
|
||||
@@ -214,8 +214,8 @@ SystemSetting(
|
||||
Setting('setting_limits', 'limit.problem_files_max_size', 256 * 1024 * 1024, 'number', 'limit.problem_files_max_size', 'Max files size per problem'),
|
||||
Setting('setting_limits', 'limit.user_files', 100, 'number', 'limit.user_files', 'Max files for user'),
|
||||
Setting('setting_limits', 'limit.user_files_size', 128 * 1024 * 1024, 'number', 'limit.user_files_size', 'Max total file size for user'),
|
||||
Setting('setting_limits', 'limit.contest_files', 100, 'number', 'limit.contest_files', 'Max files for contest'),
|
||||
Setting('setting_limits', 'limit.contest_files_size', 128 * 1024 * 1024, 'number', 'limit.contest_files_size', 'Max total file size for contest'),
|
||||
Setting('setting_limits', 'limit.contest_files', 100, 'number', 'limit.contest_files', 'Max files for contest or training'),
|
||||
Setting('setting_limits', 'limit.contest_files_size', 128 * 1024 * 1024, 'number', 'limit.contest_files_size', 'Max total file size for contest or training'),
|
||||
Setting('setting_limits', 'limit.submission', 60, 'number', 'limit.submission', 'Max submission count per minute'),
|
||||
Setting('setting_limits', 'limit.submission_user', 15, 'number', 'limit.submission_user', 'Max submission count per user per minute'),
|
||||
Setting('setting_limits', 'limit.pretest', 60, 'number', 'limit.pretest', 'Max pretest count per minute'),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/import-qduoj",
|
||||
"version": "1.5.0",
|
||||
"version": "1.5.1",
|
||||
"description": "Import QDUOJ problem export",
|
||||
"main": "index.ts",
|
||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||
|
||||
@@ -2,6 +2,7 @@ export * from './utils';
|
||||
export { default as Notification } from './components/notification';
|
||||
export * from './components/dialog';
|
||||
export * as bus from './bus';
|
||||
export { default as AnsiUp } from 'ansi_up';
|
||||
export { default as loadMonaco } from './components/monaco/loader';
|
||||
export * as redux from 'react-redux';
|
||||
export * from './components/zipDownloader';
|
||||
|
||||
@@ -105,6 +105,7 @@ module.exports = function plugin(md) {
|
||||
const katexInline = function (latex) {
|
||||
options.displayMode = false;
|
||||
try {
|
||||
latex = latex.replace(/\\def{\\([a-zA-Z0-9]+)}/g, '\\def\\$1');
|
||||
return katex.renderToString(latex, options);
|
||||
} catch (error) {
|
||||
if (options.throwOnError) logger.error(error);
|
||||
@@ -117,6 +118,7 @@ module.exports = function plugin(md) {
|
||||
const katexBlock = function (latex) {
|
||||
options.displayMode = true;
|
||||
try {
|
||||
latex = latex.replace(/\\def{\\([a-zA-Z0-9]+)}/g, '\\def\\$1');
|
||||
return `<p>${katex.renderToString(latex, options)}</p>`;
|
||||
} catch (error) {
|
||||
if (options.throwOnError) logger.error(error);
|
||||
|
||||
@@ -2,23 +2,36 @@
|
||||
display: none !important
|
||||
|
||||
.autocomplete-wrapper
|
||||
width: 300px
|
||||
border: 1px solid #d9d9d9
|
||||
background-color: #fff
|
||||
padding: 1px
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
overflow-y: auto
|
||||
|
||||
form-styles()
|
||||
background-color: $input-background-color
|
||||
outline: $input-outline
|
||||
transition: outline-color .2s, border-color .2s
|
||||
transition-timing-function: ease-out-cubic
|
||||
|
||||
&:focus, &.focused
|
||||
border-color: $input-focus-border-color
|
||||
outline: $input-focus-outline
|
||||
outline-offset: 0
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2)
|
||||
|
||||
// Overwrite, for IE compatibility
|
||||
&:read-only
|
||||
outline: 0
|
||||
|
||||
&:disabled
|
||||
opacity: 0.5
|
||||
|
||||
&:hover
|
||||
border-color: #40a9ff
|
||||
|
||||
&.focused
|
||||
border-color: #40a9ff
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2)
|
||||
|
||||
input
|
||||
background-color: #fff
|
||||
background-color: $input-background-color
|
||||
color: rgba(0,0,0,.85)
|
||||
height: 30px
|
||||
box-sizing: border-box
|
||||
|
||||
@@ -274,7 +274,7 @@ const AutoComplete = forwardRef(function Impl<T>(props: AutoCompleteProps<T>, re
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'inline-block', width: '100%' }}>
|
||||
<div style={{ display: 'inline-block', width: '100%', marginBottom: '1rem' }}>
|
||||
<div
|
||||
className={focused ? 'autocomplete-wrapper focused' : 'autocomplete-wrapper'}
|
||||
style={{ width, height }}
|
||||
|
||||
@@ -3,16 +3,15 @@
|
||||
$checkbox-size = 16px
|
||||
|
||||
.checkbox
|
||||
font-size: rem($font-size-secondary)
|
||||
line-height: 1.4
|
||||
font-size: rem($font-size-small) !important
|
||||
line-height: 1.4 !important
|
||||
border: 0
|
||||
display: inline-block
|
||||
display: inline-block !important
|
||||
vertical-align: middle
|
||||
cursor: pointer
|
||||
user-select: none
|
||||
color: lighten($text-1-color, 30%)
|
||||
margin: $input-margin
|
||||
padding: ((rem($form-control-height) - (@line-height * @font-size)) / 2) 0
|
||||
color: lighten($text-1-color, 30%) !important
|
||||
margin: $input-margin !important
|
||||
|
||||
&:hover
|
||||
color: $text-1-color
|
||||
@@ -20,13 +19,12 @@ $checkbox-size = 16px
|
||||
input
|
||||
appearance: none
|
||||
border: 1px solid #D0D0D0
|
||||
display: inline-block
|
||||
border-radius: 1px
|
||||
vertical-align: top
|
||||
width: rem($checkbox-size)
|
||||
height: rem($checkbox-size)
|
||||
margin: ((@line-height * @font-size - rem($checkbox-size)) / 2) 0
|
||||
margin-right: rem(5px)
|
||||
margin: ((@line-height * @font-size - rem($checkbox-size)) / 2) 0 !important
|
||||
margin-right: rem(5px) !important
|
||||
position: relative
|
||||
outline: 0
|
||||
transition: background .1s linear
|
||||
@@ -65,13 +63,14 @@ $checkbox-size = 16px
|
||||
margin-bottom: 0
|
||||
line-height: 1.2
|
||||
padding: ((rem($compact-control-height) - (@line-height * @font-size)) / 2) 0
|
||||
margin: 0 !important
|
||||
|
||||
input
|
||||
margin: ((@line-height * @font-size - rem($checkbox-size)) / 2) 0
|
||||
margin-right: rem(5px)
|
||||
|
||||
label.checkbox.inverse
|
||||
color: rgba($immersive-text-color, 0.8)
|
||||
color: rgba($immersive-text-color, 0.8) !important
|
||||
|
||||
input
|
||||
color: $immersive-text-color
|
||||
|
||||
@@ -299,8 +299,7 @@ Evaluated difficulty: 평가된 난이도
|
||||
expand: 확장
|
||||
Expire: 만료
|
||||
Export All Code: 모든 코드 내보내기
|
||||
Export as CSV: CSV로 내보내기
|
||||
Export as HTML: HTML로 내보내기
|
||||
Export as {0}: '{0}로 내보내기'
|
||||
Extension (days): 추가 (날짜)
|
||||
Extension Score Penalty: 추가 패널티
|
||||
Failed to join the domain. You are already a member.: 그룹 참여 실패. 이미 참여함.
|
||||
|
||||
@@ -59,6 +59,7 @@ Accepted Problems: 通过的题目
|
||||
Accepted Ratio: 通过率
|
||||
Accepted: 已通过
|
||||
Access Control: 访问控制
|
||||
According to the contest rules, you cannot view your submission details at current.: 根据比赛规则,你现在不能查看你的评测记录详情。
|
||||
Account Settings: 账户设置
|
||||
Action: 动作
|
||||
Active Sessions: 活动会话
|
||||
@@ -73,7 +74,10 @@ All Homeworks: 所有作业
|
||||
All Languages: 所有语言
|
||||
All Submissions: 所有递交
|
||||
All Training Plans: 所有训练计划
|
||||
All Users: 所有用户
|
||||
All: 全部
|
||||
Allow to maintain this contest: 允许管理此比赛
|
||||
Allow user view others code after contest: 允许用户赛后查看其他人的代码
|
||||
Always granted all privileges: 总是拥有所有权限
|
||||
An error has occurred.: 发生了一个错误。
|
||||
An example of dataset: 测试数据集的一个例子
|
||||
@@ -82,14 +86,18 @@ Any user is allowed to join this domain: 任意用户都可以加入此域
|
||||
API: API
|
||||
Argument {0} is invalid.: 非法的参数 {0} 。
|
||||
Arguments: 参数
|
||||
Assign User or Group: 分配给用户或小组
|
||||
Assign: 分配给
|
||||
Assignment: 作业
|
||||
At least 4 characters: 至少 4 个字符
|
||||
Attend Contest: 参加比赛
|
||||
Attend contests: 参加比赛
|
||||
Attended: 已参加
|
||||
Attendee Manage: 参赛者管理
|
||||
author: 作者
|
||||
Auto hide problems during the contest: 在比赛过程中自动隐藏题目
|
||||
Auto Read Tasks: 自动识别任务
|
||||
Basic Info: 基础信息
|
||||
Be Copied: 被复制
|
||||
Begin at: 开始于
|
||||
Begin Date: 开始日期
|
||||
@@ -131,6 +139,7 @@ Claimed: 已认领
|
||||
Clear Marks: 清除标记
|
||||
Click here to chat with me: 点击这里与我聊天
|
||||
click to add: 点击添加
|
||||
Clone: 复制
|
||||
CLOSE: 关闭
|
||||
Code blocks: 代码区块
|
||||
Code language: 代码语言
|
||||
@@ -159,8 +168,10 @@ Contact: 联系
|
||||
Content copied to clipboard!: 内容已复制到剪贴板!
|
||||
content: 内容
|
||||
Content: 内容
|
||||
Contest Maintainer: 比赛管理员
|
||||
Contest scoreboard is not visible.: 当前比赛成绩表隐藏,暂不可显示。
|
||||
Contest Scoreboard: 比赛成绩表
|
||||
Contest Settings: 比赛设置
|
||||
contest_create: 创建比赛
|
||||
contest_detail_problem_submit: 递交比赛题目
|
||||
contest_edit: 编辑比赛
|
||||
@@ -328,8 +339,7 @@ Exit: 退出
|
||||
expand: 展开
|
||||
Expire: 过期
|
||||
Export All Code: 导出所有代码
|
||||
Export as CSV: 导出为 CSV
|
||||
Export as HTML: 导出为 HTML
|
||||
Export as {0}: 导出为 {0}
|
||||
Extension (days): 最长延期 (日)
|
||||
Extension Score Penalty: 延期递交扣分规则
|
||||
Extra Files Config: 额外文件设置
|
||||
@@ -452,6 +462,7 @@ LaTeX Expressions: LaTeX 表达式
|
||||
Learn More: 了解更多
|
||||
Leave blank to use numeric pid.: 留空来使用数字题号。
|
||||
Light: 亮色
|
||||
Limit user to finish contest within N hours: 限制用户在N小时内完成比赛
|
||||
Limitations: 限制
|
||||
Links: 链接
|
||||
List View: 列表视图
|
||||
@@ -460,6 +471,7 @@ Live...: 正在进行…
|
||||
Loading editor...: 正在加载编辑器...
|
||||
Loading file...: 正在加载文件内容...
|
||||
Location: 位置
|
||||
Lock scoreboard on last N minutes: 在赛事最后N分钟时锁定成绩表
|
||||
Login to Attend Contest: 登录后参加比赛
|
||||
Login to Claim Homework: 登录后认领作业
|
||||
Login to Create a Discussion: 登录后创建讨论
|
||||
@@ -541,6 +553,7 @@ objective: 客观题
|
||||
Oh, the user doesn't have any contributions!: 啊哦,这个用户还没贡献过题目~
|
||||
Oh, the user hasn't created any discussions yet!: 这个用户还没有发布过讨论
|
||||
Oh, the user hasn't submitted yet!: 这个用户还没有交过题 _(:зゝ∠)_
|
||||
Oh, there is no submission!: 喔,还没有评测记录呢!
|
||||
Oh, there is no task in the queue!: 喔,队列中目前没有任务。
|
||||
Oh, there is no task that matches the filter!: 喔,目前没有符合过滤条件的任务。
|
||||
Ok: 确定
|
||||
@@ -582,6 +595,7 @@ perm_problem_solution: 题解
|
||||
perm_problem: 题库
|
||||
perm_record: 记录
|
||||
perm_training: 训练
|
||||
Permission Control: 权限控制
|
||||
Permission: 权限
|
||||
Permissions: 权限
|
||||
Pick a problem randomly based on current filter: 根据当前过滤条件随机选择一道题
|
||||
@@ -647,6 +661,7 @@ QQ Visibility: QQ 可见性
|
||||
Quit Scratchpad: 退出在线编程模式
|
||||
Quote: 引用
|
||||
Rank: 排名
|
||||
Ranked Users: 排名用户
|
||||
ranking: 排名
|
||||
Read data of own problems: 读取自己题目的数据
|
||||
Read data of problem: 读取题目数据
|
||||
@@ -686,6 +701,7 @@ Reply problem solutions: 回复题解
|
||||
reply: 回复
|
||||
Reply: 回复
|
||||
Report: 举报
|
||||
Require Invitation Code: 需要邀请码
|
||||
required: 必填
|
||||
Reset Password or Find Username: 重置密码或找回用户名
|
||||
Reset Password: 重置密码
|
||||
@@ -769,6 +785,7 @@ Sort by: 排序
|
||||
Source type: 来源类型
|
||||
Star Topic: 收藏讨论
|
||||
Star: 收藏
|
||||
Starred Users: 关注用户
|
||||
Start at: 开始于
|
||||
Statistics: 统计
|
||||
Status is hidden: 状态已隐藏
|
||||
@@ -853,6 +870,7 @@ Unknown field {0}.: 未知字段 {0}。
|
||||
Unknown: 未知
|
||||
Unlimited: 无限制
|
||||
Unordered List: 无序列表
|
||||
Unrank: 打星
|
||||
Unstar Topic: 取消收藏
|
||||
Unstar: 取消收藏
|
||||
Unsupported configure this type of problem. Please refer to the documentation.: 暂不支持快捷配置此类题目。请参照文档操作。
|
||||
@@ -917,6 +935,7 @@ View Problem: 查看题目
|
||||
View problems: 查看题目
|
||||
View this domain: 查看此域
|
||||
View training plans: 查看训练计划
|
||||
View Training: 查看训练计划
|
||||
View: 查看
|
||||
Visible to registered users: 对注册用户可见
|
||||
Visit: 查看
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/ui-default",
|
||||
"version": "4.43.0",
|
||||
"version": "4.44.0",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "index.ts",
|
||||
|
||||
+52
@@ -107,3 +107,55 @@ $highlight-button-color = #F6DF45
|
||||
|
||||
.detail-button
|
||||
width: auto
|
||||
|
||||
.page--contest_problemlist
|
||||
.col--status
|
||||
width: rem(150px)
|
||||
|
||||
.col--submit-at
|
||||
width: rem(150px)
|
||||
|
||||
.col--problem
|
||||
border-left: 1px solid $table-border-color
|
||||
|
||||
+mobile()
|
||||
.col--submit-at
|
||||
display: none
|
||||
|
||||
|
||||
.page--contest_scoreboard
|
||||
.main
|
||||
.row
|
||||
max-width: 100vw !important;
|
||||
|
||||
.col--rank
|
||||
width: rem(60px)
|
||||
text-align: center
|
||||
border-right: 1px solid $table-border-color
|
||||
|
||||
.col--user
|
||||
min-width: rem(150px)
|
||||
|
||||
.col--solved, .col--time
|
||||
width: rem(70px)
|
||||
text-align: center
|
||||
|
||||
.col--total_score
|
||||
width: rem(100px)
|
||||
text-align: center
|
||||
|
||||
.col--problem
|
||||
width: rem(100px)
|
||||
border-left: 1px solid $table-border-color
|
||||
text-align: center
|
||||
|
||||
.scoreboard--acm
|
||||
.col--problem
|
||||
width: rem(70px) !important
|
||||
|
||||
+mobile()
|
||||
.col--user
|
||||
width: auto
|
||||
|
||||
.col--problem
|
||||
display: none
|
||||
@@ -1,13 +0,0 @@
|
||||
.page--contest_detail
|
||||
.col--status
|
||||
width: rem(150px)
|
||||
|
||||
.col--submit-at
|
||||
width: rem(150px)
|
||||
|
||||
.col--problem
|
||||
border-left: 1px solid $table-border-color
|
||||
|
||||
+mobile()
|
||||
.col--submit-at
|
||||
display: none
|
||||
@@ -1,6 +0,0 @@
|
||||
.page--contest_edit
|
||||
.page--contest_create
|
||||
label
|
||||
&.checkbox
|
||||
margin-top: 24px
|
||||
|
||||
@@ -1,17 +1,41 @@
|
||||
import $ from 'jquery';
|
||||
import moment from 'moment';
|
||||
import ProblemSelectAutoComplete from 'vj/components/autocomplete/ProblemSelectAutoComplete';
|
||||
import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete';
|
||||
import { ConfirmDialog } from 'vj/components/dialog';
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import { i18n, request, tpl } from 'vj/utils';
|
||||
|
||||
const page = new NamedPage(['contest_edit', 'contest_create', 'homework_create', 'homework_edit'], (pagename) => {
|
||||
ProblemSelectAutoComplete.getOrConstruct($('[name="pids"]'), { multi: true, clearDefaultValue: false });
|
||||
UserSelectAutoComplete.getOrConstruct<true>($('[name="maintainer"]'), { multi: true, clearDefaultValue: false });
|
||||
$('[name="rule"]').on('change', () => {
|
||||
const rule = $('[name="rule"]').val();
|
||||
$('.contest-rule-settings input').attr('disabled', 'disabled');
|
||||
$('.contest-rule-settings').hide();
|
||||
$(`.contest-rule--${rule} input`).removeAttr('disabled');
|
||||
$(`.contest-rule--${rule}`).show();
|
||||
}).trigger('change');
|
||||
$('[name="beginAtDate"], [name="beginAtTime"], [name="duration"]').on('change', () => {
|
||||
const beginAtDate = $('[name="beginAtDate"]').val();
|
||||
const beginAtTime = $('[name="beginAtTime"]').val();
|
||||
const duration = $('[name="duration"]').val();
|
||||
const endAt = moment(`${beginAtDate} ${beginAtTime}`).add(+duration, 'hours').toDate();
|
||||
if (endAt) $('[name="endAt"]').val(moment(endAt).format('YYYY-MM-DD HH:mm'));
|
||||
});
|
||||
$('[name="permission"]').removeAttr('disabled').on('change', () => {
|
||||
const type = $('[name="permission"]').val();
|
||||
$('[data-perm] input').attr('disabled', 'disabled');
|
||||
$('[data-perm]').hide();
|
||||
$(`[data-perm="${type}"] input`).removeAttr('disabled');
|
||||
$(`[data-perm="${type}"]`).show();
|
||||
}).trigger('change');
|
||||
if (pagename.endsWith('edit')) {
|
||||
let confirmed = false;
|
||||
$(document).on('click', '[name="operation"]', (ev) => {
|
||||
$(document).on('click', '[value="delete"]', (ev) => {
|
||||
ev.preventDefault();
|
||||
if (confirmed) {
|
||||
return request.post('.', { operation: 'delete' }).then((res) => {
|
||||
return request.post('', { operation: 'delete' }).then((res) => {
|
||||
window.location.href = res.url;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
.page--contest_scoreboard
|
||||
.main
|
||||
.row
|
||||
max-width: 100vw !important;
|
||||
|
||||
.col--rank
|
||||
width: rem(60px)
|
||||
text-align: center
|
||||
border-right: 1px solid $table-border-color
|
||||
|
||||
.col--user
|
||||
min-width: rem(150px)
|
||||
|
||||
.col--solved, .col--time
|
||||
width: rem(70px)
|
||||
text-align: center
|
||||
|
||||
.col--total_score
|
||||
width: rem(100px)
|
||||
text-align: center
|
||||
|
||||
.col--problem
|
||||
width: rem(100px)
|
||||
border-left: 1px solid $table-border-color
|
||||
text-align: center
|
||||
|
||||
.scoreboard--acm
|
||||
.col--problem
|
||||
width: rem(70px) !important
|
||||
|
||||
+mobile()
|
||||
.col--user
|
||||
width: auto
|
||||
|
||||
.col--problem
|
||||
display: none
|
||||
@@ -0,0 +1,39 @@
|
||||
import $ from 'jquery';
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
|
||||
const page = new NamedPage('contest_scoreboard', () => {
|
||||
const { tdoc } = UiContext;
|
||||
const key = `scoreboard-star/${tdoc.domainId}/${tdoc.docId}`;
|
||||
const read = () => JSON.parse(localStorage.getItem(key) || '[]');
|
||||
const write = (data) => localStorage.setItem(key, JSON.stringify(data));
|
||||
|
||||
read().forEach((uid) => $(`.star.user--${uid}`).addClass('activated'));
|
||||
$('.star').on('click', (e) => {
|
||||
const star = read();
|
||||
const $target = $(e.currentTarget);
|
||||
const uid = $target.data('uid');
|
||||
if (star.includes(uid)) {
|
||||
$target.removeClass('activated');
|
||||
star.splice(star.indexOf(uid), 1);
|
||||
} else {
|
||||
$target.addClass('activated');
|
||||
star.push(uid);
|
||||
}
|
||||
write(star);
|
||||
});
|
||||
|
||||
$('.select.filter').on('change', (e) => {
|
||||
const val = $(e.target).val();
|
||||
if (val === 'all') {
|
||||
$('.data-table tbody tr').show();
|
||||
} else if (val === 'star') {
|
||||
$('.data-table tbody tr').hide();
|
||||
read().forEach((uid) => $(`.star.user--${uid}`).closest('tr').show());
|
||||
} else {
|
||||
$('.data-table tbody tr').show();
|
||||
$('.rank--unrank').closest('tr').hide();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -0,0 +1,97 @@
|
||||
import $ from 'jquery';
|
||||
import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete';
|
||||
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';
|
||||
|
||||
const page = new NamedPage('contest_user', () => {
|
||||
const addUserDialogContent = $(tpl`
|
||||
<div>
|
||||
<div class="row"><div class="columns">
|
||||
<h1>${i18n('Add User')}</h1>
|
||||
</div></div>
|
||||
<div class="row"><div class="columns">
|
||||
<label>
|
||||
${i18n('Users')}
|
||||
<input name="add_user_users" type="text" class="textbox" autocomplete="off">
|
||||
</label>
|
||||
</div></div>
|
||||
<div class="row"><div class="columns">
|
||||
<label>
|
||||
${i18n('Rank')}
|
||||
<br />
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="unrank" class="checkbox">${i18n('UnRank')}
|
||||
</label>
|
||||
</label>
|
||||
</div></div>
|
||||
</div>
|
||||
`);
|
||||
addUserDialogContent.appendTo(document.body);
|
||||
const userSelect = UserSelectAutoComplete.getOrConstruct<UserSelectAutoComplete<true>>(
|
||||
addUserDialogContent.find('[name="add_user_users"]'),
|
||||
{ multi: true, height: 'auto' },
|
||||
);
|
||||
|
||||
const addUserDialog = new ActionDialog({
|
||||
$body: addUserDialogContent,
|
||||
onDispatch(action) {
|
||||
if (action === 'ok' && !userSelect.value()) {
|
||||
userSelect.focus();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
addUserDialog.clear = function () {
|
||||
userSelect.clear();
|
||||
addUserDialog.$dom.find('[name="unrank"]').prop('checked', false);
|
||||
return this;
|
||||
};
|
||||
|
||||
async function handleClickAddUser() {
|
||||
const action = await addUserDialog.clear().open();
|
||||
if (action !== 'ok') return;
|
||||
const unrank = addUserDialog.$dom.find('[name="unrank"]').prop('checked');
|
||||
const uids = userSelect.value();
|
||||
try {
|
||||
const res = await request.post('', {
|
||||
operation: 'add_user',
|
||||
uids: uids.join(','),
|
||||
unrank,
|
||||
});
|
||||
if (res.url && res.url !== window.location.href) window.location.href = res.url;
|
||||
else {
|
||||
Notification.success(i18n('User has been added.'));
|
||||
pjax.request({ push: false });
|
||||
}
|
||||
} catch (error) {
|
||||
Notification.error([error.message, ...error.params].join(' '));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEditRank(ev) {
|
||||
const uid = $(ev.target).data('uid');
|
||||
try {
|
||||
const res = await request.post('', {
|
||||
operation: 'rank',
|
||||
uid,
|
||||
});
|
||||
if (res.url && res.url !== window.location.href) window.location.href = res.url;
|
||||
else {
|
||||
Notification.success(i18n('User Rank has been edited.'));
|
||||
pjax.request({ push: false });
|
||||
}
|
||||
} catch (error) {
|
||||
Notification.error([error.message, ...error.params].join(' '));
|
||||
}
|
||||
}
|
||||
|
||||
$('[name="add_user"]').on('click', () => handleClickAddUser());
|
||||
$(document).on('click', '[name="edit_rank"]', handleEditRank);
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -92,7 +92,7 @@ function handleDrop(ev) {
|
||||
handleClickUpload(files);
|
||||
}
|
||||
|
||||
const page = new NamedPage(['home_files', 'contest_files'], () => {
|
||||
const page = new NamedPage(['home_files', 'contest_files', 'training_files'], () => {
|
||||
$(document).on('click', '[name="upload_file"]', () => handleClickUpload());
|
||||
$(document).on('click', '[name="remove_selected"]', () => handleClickRemoveSelected());
|
||||
$(document).on('dragover', '.files', (ev) => handleDragOver(ev));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.page--home_files,.page--contest_files
|
||||
.page--home_files,.page--contest_files,.page--training_files
|
||||
.col--checkbox
|
||||
width: 60px
|
||||
|
||||
|
||||
@@ -199,13 +199,13 @@
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="{{ args.name }}"{% if args.value %} checked{% endif %}
|
||||
{{ form_attr('checkbox', args) }}
|
||||
>{{ _(args.label) }}
|
||||
>{% if args.placeholder %}{{ _(args.placeholder) }}{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro form_checkbox(args) %}
|
||||
{{ form_begin({columns:5, no_label:true}|assign(args)) }}
|
||||
{{ form_begin({columns:5}|assign(args)) }}
|
||||
{{ checkbox(args) }}
|
||||
{{ form_end({no_label:true}|assign(args)) }}
|
||||
{{ form_end(args) }}
|
||||
{% endmacro %}
|
||||
@@ -37,66 +37,6 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section__header">
|
||||
<h1 class="section__title">{{ _('Problem') }}</h1>
|
||||
</div>
|
||||
{% if pdict %}
|
||||
<div class="section__body no-padding">
|
||||
<table class="data-table">
|
||||
<colgroup>
|
||||
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
|
||||
<col class="col--status">
|
||||
<col class="col--submit-at">
|
||||
{% endif %}
|
||||
<col class="col--problem">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
|
||||
<th class="col--status record-status--border">{{ _('Status') }}</th>
|
||||
<th class="col--submit-at">{{ _('Last Submit At') }}</th>
|
||||
{% endif %}
|
||||
<th class="col--problem">{{ _('Problem') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% set isAdmin = handler.user.own(tdoc) or handler.user.hasPerm(perm.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD) %}
|
||||
{% set ntdoc = model.contest.isDone(tdoc) or (tsdoc.attend and not model.contest.isNotStarted(tdoc)) %}
|
||||
{%- for pid in tdoc.pids -%}
|
||||
<tr>
|
||||
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
|
||||
{% if psdict[pid] and psdict[pid].rid %}
|
||||
{% set rdoc = rdict[psdict[pid].rid] %}
|
||||
{% if model.contest.canShowSelfRecord.call(handler, tdoc) %}
|
||||
{{ record.render_status_td(rdoc) }}
|
||||
{% else %}
|
||||
<td class="col--status record-status--border"><a href="{{ url('record_detail', rid=rdoc._id) }}">{{ _('Submitted') }}</a></td>
|
||||
{% endif %}
|
||||
<td class="col--submit-at">{{ datetimeSpan(rdoc._id)|safe }}</td>
|
||||
{% else %}
|
||||
<td class="col--status record-status--border">{{ _('No Submissions') }}</td>
|
||||
<td class="col--submit-at">-</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<td class="col--problem col--problem-name">
|
||||
{% if isAdmin and not ntdoc %}
|
||||
{{ problem.render_problem_title(pdict[pid], show_invisible_flag=false, show_tags=false) }}
|
||||
{% else %}
|
||||
{{ problem.render_problem_title(pdict[pid], tdoc=tdoc, show_invisible_flag=false, show_tags=false) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% elif tsdoc.attend %}
|
||||
{{ nothing.render('This contest is not live.') }}
|
||||
{% else %}
|
||||
{{ nothing.render('Please attend contest to see the problems.') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="medium-3 columns">
|
||||
{% set owner_udoc = udict[tdoc.owner] %}
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
<div class="row">
|
||||
<div class="medium-9 columns">
|
||||
<div class="section">
|
||||
<div class="section__body">
|
||||
<form method="post">
|
||||
<form method="post">
|
||||
<div class="section__header">
|
||||
<h2 class="section__title">{{ _('Basic Info') }}</h2>
|
||||
</div>
|
||||
<div class="section__body">
|
||||
<div class="row">
|
||||
{{ form.form_select({
|
||||
columns:3,
|
||||
@@ -15,7 +18,7 @@
|
||||
row:false
|
||||
}) }}
|
||||
{{ form.form_text({
|
||||
columns:4,
|
||||
columns:9,
|
||||
label:'Title',
|
||||
name:'title',
|
||||
placeholder:_('title'),
|
||||
@@ -23,22 +26,6 @@
|
||||
autofocus:true,
|
||||
row:false
|
||||
}) }}
|
||||
{{ form.form_text({
|
||||
columns:3,
|
||||
label:'Invitation Code',
|
||||
name:'code',
|
||||
placeholder:_('(leave blank if none)'),
|
||||
value:tdoc._code|default(''),
|
||||
row:false
|
||||
}) }}
|
||||
{{ form.form_checkbox({
|
||||
columns:2,
|
||||
label:'Rated',
|
||||
name:'rated',
|
||||
placeholder:_('Is rated'),
|
||||
value:tdoc.rated|default(true),
|
||||
row:false
|
||||
}) }}
|
||||
</div>
|
||||
<div class="row">
|
||||
{{ form.form_text({
|
||||
@@ -68,56 +55,151 @@
|
||||
}) }}
|
||||
{{ form.form_text({
|
||||
columns:4,
|
||||
label:'Assign',
|
||||
name:'assign',
|
||||
placeholder:_('Group / UID'),
|
||||
value:tdoc.assign|default(''),
|
||||
label:'End Time',
|
||||
name:'endAt',
|
||||
placeholder:'YYYY-mm-dd HH:MM',
|
||||
value:beginAt.add(duration, 'hours').format('YYYY-M-D HH:mm'),
|
||||
disabled:true,
|
||||
row:false
|
||||
}) }}
|
||||
</div>
|
||||
<div class="row">
|
||||
{{ form.form_text({
|
||||
columns:9,
|
||||
columns:12,
|
||||
label:'Problems',
|
||||
name:'pids',
|
||||
value:pids,
|
||||
placeholder:_("Seperated with ','"),
|
||||
row:false
|
||||
row:true
|
||||
}) }}
|
||||
{{ form.form_checkbox({
|
||||
columns:3,
|
||||
label:'Auto hide-and show',
|
||||
name:'autoHide',
|
||||
placeholder:'',
|
||||
value:handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) and (tdoc.autoHide|default(true)),
|
||||
disabled:not handler.user.hasPerm(perm.PERM_EDIT_PROBLEM),
|
||||
row:false
|
||||
}) }}
|
||||
{% if tdoc.lockAt %}
|
||||
<input type="hidden" name="lock" value="{{ ((tdoc.endAt - tdoc.lockAt)/60000)|int }}">
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ form.form_textarea({
|
||||
columns:null,
|
||||
label:'Content',
|
||||
label:'Description',
|
||||
name:'content',
|
||||
value:tdoc['content']|default(''),
|
||||
markdown:true
|
||||
}) }}
|
||||
</div>
|
||||
<div style="padding-top: 0" class="section__header">
|
||||
<h2 class="section__title">{{ _('Permission Control') }}</h2>
|
||||
</div>
|
||||
<div class="section__body">
|
||||
{{ form.form_text({
|
||||
columns:12,
|
||||
label:'Contest Maintainer',
|
||||
name:'maintainer',
|
||||
help_text:_('Allow to maintain this contest'),
|
||||
value:tdoc.maintainer|default('')
|
||||
}) }}
|
||||
<div class="row">
|
||||
{{ form.form_select({
|
||||
columns:3,
|
||||
label:'Permission Control',
|
||||
name:'permission',
|
||||
options:{
|
||||
'public': _('Public'),
|
||||
'invite': _('Require Invitation Code'),
|
||||
'assign': _('Assign User or Group')
|
||||
},
|
||||
value: ("public" if not tdoc._code else "invite") if not tdoc.assign else "assign",
|
||||
disabled:true,
|
||||
row:false
|
||||
}) }}
|
||||
<div style="display: none" data-perm="assign">
|
||||
{{ form.form_text({
|
||||
columns:9,
|
||||
label:'Assign',
|
||||
name:'assign',
|
||||
placeholder:_('Group / UID'),
|
||||
value:tdoc.assign|default(''),
|
||||
disabled:true,
|
||||
row:false
|
||||
}) }}
|
||||
</div>
|
||||
<div style="display: none" data-perm="invite">
|
||||
{{ form.form_text({
|
||||
columns:9,
|
||||
label:'Invitation Code',
|
||||
name:'code',
|
||||
placeholder:_('(leave blank if none)'),
|
||||
value:tdoc._code|default(''),
|
||||
disabled:true,
|
||||
row:false
|
||||
}) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding-top: 0" class="section__header">
|
||||
<h2 class="section__title">{{ _('Contest Settings') }}</h2>
|
||||
</div>
|
||||
<div class="section__body">
|
||||
<div class="row">
|
||||
{{ form.form_checkbox({
|
||||
columns:2,
|
||||
label:'Rated',
|
||||
name:'rated',
|
||||
placeholder:_('Is rated'),
|
||||
value:tdoc.rated|default(true),
|
||||
row:false
|
||||
}) }}
|
||||
{{ form.form_checkbox({
|
||||
columns:3,
|
||||
label:'Auto hide-and show',
|
||||
name:'autoHide',
|
||||
placeholder:_('Auto hide problems during the contest'),
|
||||
value:handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) and (tdoc.autoHide|default(true)),
|
||||
disabled:not handler.user.hasPerm(perm.PERM_EDIT_PROBLEM),
|
||||
row:false
|
||||
}) }}
|
||||
{{ form.form_checkbox({
|
||||
columns:3,
|
||||
label:'Allow View Code',
|
||||
name:'allowViewCode',
|
||||
placeholder:_('Allow user view others code after contest'),
|
||||
value:tdoc.allowViewCode|default(true),
|
||||
row:false
|
||||
}) }}
|
||||
<div style="display: none" class="contest-rule-settings contest-rule--acm">
|
||||
{{ form.form_text({
|
||||
columns:4,
|
||||
label:'Lock (minutes)',
|
||||
name:'lock',
|
||||
placeholder:_('(leave blank if none)'),
|
||||
value:(((tdoc.endAt - tdoc.lockAt)/60000)|int) if tdoc.lockAt else '',
|
||||
help_text:_('Lock scoreboard on last N minutes'),
|
||||
disabled:true,
|
||||
row:false
|
||||
}) }}
|
||||
</div>
|
||||
<div style="display: none" class="contest-rule-settings contest-rule--oi">
|
||||
{{ form.form_text({
|
||||
columns:4,
|
||||
label:'Flexable Duration',
|
||||
name:'contestDuration',
|
||||
placeholder:_('(leave blank if none)'),
|
||||
value:tdoc.duration|default(''),
|
||||
help_text:_('Limit user to finish contest within N hours'),
|
||||
disabled:true,
|
||||
row:false
|
||||
}) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="columns">
|
||||
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
|
||||
{% if page_name == 'contest_edit' %}
|
||||
<button type="submit" class="rounded primary button">
|
||||
<button name="operation" value="update" type="submit" class="rounded primary button">
|
||||
{{ _('Update') }}
|
||||
</button>
|
||||
<button name="operation" value="update" type="submit" formaction="{{ url('contest_create') }}" class="rounded button">
|
||||
{{ _('Clone') }}
|
||||
</button>
|
||||
{% if handler.user.own(tdoc) or handler.user.hasPerm(perm.PERM_EDIT_CONTEST) %}
|
||||
<button name="operation" value="delete" type="submit" class="rounded button">
|
||||
{{ _('Delete') }}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<button type="submit" class="rounded primary button">
|
||||
<button name="operation" value="update" type="submit" class="rounded primary button">
|
||||
{{ _('Create') }}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
{% extends "layout/basic.html" %}
|
||||
{% import "components/contest.html" as contest with context %}
|
||||
{% import "components/record.html" as record with context %}
|
||||
{% import "components/problem.html" as problem with context %}
|
||||
{% block content %}
|
||||
{{ set(UiContext, 'tdoc', tdoc) }}
|
||||
<div class="row">
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
{% extends "layout/basic.html" %}
|
||||
{% import "components/contest.html" as contest with context %}
|
||||
{% import "components/record.html" as record with context %}
|
||||
{% import "components/problem.html" as problem with context %}
|
||||
{% block content %}
|
||||
{{ set(UiContext, 'tdoc', tdoc) }}
|
||||
<div class="row">
|
||||
<div class="medium-9 columns">
|
||||
<div class="section">
|
||||
<div class="section__header">
|
||||
<h1 class="section__title">{{ _('Problem') }}</h1>
|
||||
</div>
|
||||
<div class="section__body no-padding">
|
||||
<table class="data-table">
|
||||
<colgroup>
|
||||
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
|
||||
<col class="col--status">
|
||||
<col class="col--submit-at">
|
||||
{% endif %}
|
||||
<col class="col--problem">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
|
||||
<th class="col--status record-status--border">{{ _('Status') }}</th>
|
||||
<th class="col--submit-at">{{ _('Last Submit At') }}</th>
|
||||
{% endif %}
|
||||
<th class="col--problem">{{ _('Problem') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for pid in tdoc.pids -%}
|
||||
<tr>
|
||||
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
|
||||
{% if psdict[pid] and psdict[pid].rid %}
|
||||
{% set rdoc = rdict[psdict[pid].rid] %}
|
||||
{% if model.contest.canShowSelfRecord.call(handler, tdoc) %}
|
||||
{{ record.render_status_td(rdoc) }}
|
||||
{% else %}
|
||||
<td class="col--status record-status--border"><a href="{{ url('record_detail', rid=rdoc._id) }}">{{ _('Submitted') }}</a></td>
|
||||
{% endif %}
|
||||
<td class="col--submit-at">{{ datetimeSpan(rdoc._id)|safe }}</td>
|
||||
{% else %}
|
||||
<td class="col--status record-status--border">{{ _('No Submissions') }}</td>
|
||||
<td class="col--submit-at">-</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<td class="col--problem col--problem-name">
|
||||
<a href="{{ url('problem_detail', pid=pid, query={tid:tdoc.docId}) }}">
|
||||
<b>{{ String.fromCharCode(65+loop.index0) }}</b> {{ pdict[pid].title }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section__body no-padding">
|
||||
{% if not canViewRecord %}
|
||||
{{ nothing.render('According to the contest rules, you cannot view your submission details at current.') }}'
|
||||
{% elif not rdocs.length %}
|
||||
{{ nothing.render('Oh, there is no submission!') }}
|
||||
{% else %}
|
||||
<table class="data-table record_main__table">
|
||||
<colgroup>
|
||||
<col class="col--status">
|
||||
<col class="col--problem">
|
||||
<col class="col--submit-by">
|
||||
<col class="col--time">
|
||||
<col class="col--memory">
|
||||
<col class="col--lang">
|
||||
<col class="col--submit-at">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col--status record-status--border">{{ _('Status') }}</th>
|
||||
<th class="col--problem">{{ _('Problem') }}</th>
|
||||
<th class="col--submit-by">{{ _('Submit By') }}</th>
|
||||
<th class="col--time">{{ _('Time') }}</th>
|
||||
<th class="col--memory">{{ _('Memory') }}</th>
|
||||
<th class="col--lang">{{ _('Language') }}</th>
|
||||
<th class="col--submit-at">{{ _('Submit At') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for rdoc in rdocs -%}
|
||||
{% set udoc = udict[rdoc['uid']] %}
|
||||
{% set pdoc = pdict[rdoc['pid']] %}
|
||||
{% include 'record_main_tr.html' %}
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="medium-3 columns">
|
||||
{% set owner_udoc = udict[tdoc.owner] %}
|
||||
{% include "partials/contest_sidebar.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -16,17 +16,24 @@
|
||||
<div class="section__header">
|
||||
{%- for ext in ['HTML','CSV','Ghost'] -%}
|
||||
<a class="button" target="_blank" href="{{ url(type + '_scoreboard_download', tid=tdoc.docId, ext=ext.toLowerCase()) }}">
|
||||
<span class="icon icon-download"></span> {{ _('Export as '+ext) }}
|
||||
<span class="icon icon-download"></span> {{ _('Export as {0}').format(ext) }}
|
||||
</a>
|
||||
{%- endfor -%}
|
||||
{% if model.contest.isDone(tdoc) and tdoc.lockAt and handler.user.own(tdoc) and not tdoc.unlocked %}
|
||||
<form method="POST">
|
||||
<input type="hidden" name="operation" value="unlock">
|
||||
<button type="submit" class="button" href="{{ url(type + '_scoreboard_download', tid=tdoc.docId, ext='csv') }}">
|
||||
<button type="submit" class="button">
|
||||
<span class="icon icon-feeling-lucky"></span> {{ _('Unlock scoreboard') }}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<div class="section__tools">
|
||||
<select class="select filter" name="list">
|
||||
<option value="all">{{ _('All Users') }}</option>
|
||||
<option value="star">{{ _('Starred Users') }}</option>
|
||||
<option value="rank">{{ _('Ranked Users') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% if model.contest.isLocked(tdoc) %}
|
||||
<div class="section__body no-padding">
|
||||
@@ -69,8 +76,14 @@
|
||||
<tr>
|
||||
{%- for column in row -%}
|
||||
<td class="col--{{ rows[0][loop.index0]['type'] }}"{% if column.style %} style="{{ column.style }}"{% endif %}>
|
||||
{%- if column.type == 'user' -%}
|
||||
{%- if column.type == 'rank' -%}
|
||||
<span class="{{ 'rank--unrank' if column.value == '0' else 'rank--normal' }}">{{ '*' if column.value == '0' else column.value|string|nl2br|safe }}</span>
|
||||
{%- elif column.type == 'user' -%}
|
||||
{%- set canView = canViewAll or handler.user._id == column.raw -%}
|
||||
<button class="star user--{{ column.raw }}" data-uid="{{ column.raw }}">
|
||||
<span class="starred--hide"><span class="icon icon-star--outline" data-tooltip="{{ _('Star') }}"></span></span>
|
||||
<span class="starred--show"><span class="icon icon-star" data-tooltip="{{ _('Unstar') }}"></span></span>
|
||||
</button>
|
||||
{{ user.render_inline(udict[column.raw], badge=false) }}
|
||||
{%- elif column.type == 'record' and column.raw -%}
|
||||
<a href="{{ url('record_detail', rid=column.raw) if canView else '' }}" data-tooltip="{{ column.hover }}">
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
{% extends "layout/basic.html" %}
|
||||
{% import "components/contest.html" as contest with context %}
|
||||
{% import "components/record.html" as record with context %}
|
||||
{% import "components/problem.html" as problem with context %}
|
||||
{% block content %}
|
||||
{{ set(UiContext, 'tdoc', tdoc) }}
|
||||
<div class="row">
|
||||
<div class="medium-9 columns">
|
||||
<div class="section">
|
||||
<div class="section__header">
|
||||
<h1 class="section__title">{{ _('Attendee Manage') }}</h1>
|
||||
<div class="section__tools">
|
||||
<button class="primary rounded button" name="add_user"><span class="icon icon-search"></span>{{ _('Add User') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
{{ noscript_note.render() }}
|
||||
<div class="section__body no-padding">
|
||||
<table class="data-table">
|
||||
<colgroup>
|
||||
<col class="col--uid">
|
||||
<col class="col--user">
|
||||
<col class="col--start-time">
|
||||
<col class="col--end-time">
|
||||
<col class="col--rank">
|
||||
<col class="col--actions">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col--uid">{{ _('User ID') }}</th>
|
||||
<th class="col--user">{{ _('Username') }}</th>
|
||||
<td class="col--start-time">{{ _('Begin Time') }}</th>
|
||||
<th class="col--end-time">{{ _('End Time') }}</th>
|
||||
<th class="col--rank">{{ _('Rank') }}</th>
|
||||
<th class="col--actions">{{ _('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% include "partials/contest_user.html" %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="medium-3 columns">
|
||||
{% set owner_udoc = udict[tdoc.owner] %}
|
||||
{% include "partials/contest_sidebar.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -95,6 +95,9 @@
|
||||
<button type="submit" class="rounded primary button">
|
||||
{{ _('Update') }}
|
||||
</button>
|
||||
<button name="operation" value="update" type="submit" formaction="{{ url('homework_create') }}" class="rounded button">
|
||||
{{ _('Clone') }}
|
||||
</button>
|
||||
{% if handler.user.own(tdoc) or handler.user.hasPerm(perm.PERM_EDIT_HOMEWORK) %}
|
||||
<button name="operation" value="delete" type="submit" class="rounded button">
|
||||
{{ _('Delete') }}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<html data-page="{{ page_name }}" data-layout="{{ layout_name }}" class="layout--{{ layout_name }} page--{{ page_name }} theme--{{ (handler.user|default({})).theme|default('default') }} nojs" lang="{{ UserContext.viewLang }}" data-app="Hydro">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="chrome=1"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
|
||||
|
||||
@@ -75,6 +75,9 @@
|
||||
<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') }}
|
||||
</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) %}
|
||||
{% if model.contest.canShowRecord.call(handler, tdoc, true) %}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{% import "components/user.html" as user with context %}
|
||||
{% import "components/contest.html" as contest with context %}
|
||||
<tbody data-fragment-id="constest_user-tbody">
|
||||
{%- for tsdoc in tsdocs -%}
|
||||
<tr data-uid="{{ tsdoc.uid }}">
|
||||
<td class="col--uid">{{ tsdoc.uid }}</td>
|
||||
<td class="col--user">{{ user.render_inline(udict[tsdoc.uid], badge=false) }}</td>
|
||||
<td class="col--start-time">{{ contest.render_time(tsdoc.startAt or tdoc.beginAt) }}</td>
|
||||
<td class="col--end-time">{{ contest.render_time(tsdoc.endAt or tdoc.endAt) }}</td>
|
||||
<td class="col--rank">{{ _('UnRank') if tsdoc.unrank else _('Rank') }}</td>
|
||||
<td class="col--actions">
|
||||
<a name="edit_rank" data-uid="{{ tsdoc.uid }}">{{ _('Rank') if tsdoc.unrank else _('UnRank') }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
@@ -11,10 +11,9 @@
|
||||
</div>
|
||||
<div class="medium-2 columns">
|
||||
<label>
|
||||
{{ _('Highlight') }}
|
||||
<br>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="highlight" value="on"
|
||||
<input type="checkbox" name="highlight" value="on" class="checkbox"
|
||||
{%- if ddoc.highlight %} checked{% endif -%}
|
||||
{%- if not handler.user.hasPerm(perm.PERM_HIGHLIGHT_DISCUSSION) %} disabled{% endif -%}
|
||||
>{{ _('Highlight') }}
|
||||
@@ -23,10 +22,9 @@
|
||||
</div>
|
||||
<div class="medium-2 columns">
|
||||
<label>
|
||||
{{ _('Pin') }}
|
||||
<br>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="pin" value="on"
|
||||
<input type="checkbox" name="pin" value="on" class="checkbox"
|
||||
{%- if ddoc.pin %} checked{% endif -%}
|
||||
{%- if not handler.user.hasPerm(perm.PERM_PIN_DISCUSSION) %} disabled{% endif -%}
|
||||
>{{ _('Pin') }}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
</div></div>
|
||||
<div class="row"><div class="columns">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="rememberme">{{ _('Remember me') }}
|
||||
<input type="checkbox" name="rememberme" class="checkbox">{{ _('Remember me') }}
|
||||
</label>
|
||||
</div></div>
|
||||
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
{% set _val = current[setting.key] %}
|
||||
{{ form.form_checkbox({
|
||||
label:setting.name,
|
||||
help_text:setting.desc,
|
||||
placeholder:setting.desc,
|
||||
name:setting.key,
|
||||
value:setting.value if (_val === undefined or _val === null) else _val,
|
||||
disabled:setting.flag|bitand(2)
|
||||
@@ -78,7 +78,8 @@
|
||||
label:setting.name,
|
||||
name:'booleanKeys.' + setting.key,
|
||||
value:true,
|
||||
extra_class:'display-hidden'
|
||||
extra_class:'display-hidden',
|
||||
no_label:true
|
||||
})}}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
#{{ pdoc.pid|default(pdoc.docId) }}. {{ pdoc.title }}
|
||||
#{{ String.fromCharCode(65+tdoc.pids.indexOf(pdoc.docId)) if tdoc else pdoc.pid|default(pdoc.docId) }}. {{ pdoc.title }}
|
||||
</h1>
|
||||
{% if tdoc %}
|
||||
<div class="section__tools contest-problems">
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<label>
|
||||
<br>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="hidden" value="on"{% if pdoc['hidden'] %} checked{% endif %}>{{ _('Hidden') }}
|
||||
<input type="checkbox" name="hidden" value="on"{% if pdoc['hidden'] %} checked{% endif %} class="checkbox">{{ _('Hidden') }}
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -75,6 +75,9 @@
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('training_edit', tid=tdoc.docId) }}">
|
||||
<span class="icon icon-edit"></span> {{ _('Edit') }}
|
||||
</a></li>
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('training_files', tid=tdoc.docId) }}">
|
||||
<span class="icon icon-file"></span> {{ _('Files') }}
|
||||
</a></li>
|
||||
{% endif %}
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('wiki_help', anchor='training') }}">
|
||||
<span class="icon icon-help"></span> {{ _('Help') }}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="section__body">
|
||||
<form method="post">
|
||||
<div class="row">
|
||||
<div class="medium-5 columns">
|
||||
<div class="medium-10 columns">
|
||||
<label>
|
||||
{{ _('Title') }}
|
||||
<input name="title" placeholder="{{ _('title') }}" value="{{ tdoc.title|default('') }}" class="textbox" autofocus>
|
||||
@@ -16,7 +16,7 @@
|
||||
<label>
|
||||
<br>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="pin" value="on"{% if tdoc['hidden'] %} checked{% endif %}>{{ _('Pin') }}
|
||||
<input type="checkbox" name="pin" value="on"{% if tdoc['hidden'] %} checked{% endif %} class="checkbox">{{ _('Pin') }}
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
{% 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">
|
||||
<div class="section side">
|
||||
<div>
|
||||
<ol class="menu">
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('training_detail', tid=tdoc.docId) }}">
|
||||
<span class="icon icon-award"></span> {{ _('View Training') }}
|
||||
</a></li>
|
||||
{% if not tsdoc['enroll'] and handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
|
||||
<li class="menu__item">
|
||||
<form action="" method="POST">
|
||||
<input type="hidden" name="operation" value="enroll">
|
||||
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
|
||||
<button class="menu__link" type="submit">
|
||||
<span class="icon icon-add"></span> {{ _('Enroll Training') }}
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if handler.user.own(tdoc) or handler.user.hasPerm(perm.PERM_EDIT_TRAINING) %}
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('training_edit', tid=tdoc.docId) }}">
|
||||
<span class="icon icon-edit"></span> {{ _('Edit') }}
|
||||
</a></li>
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('training_files', tid=tdoc.docId) }}">
|
||||
<span class="icon icon-file"></span> {{ _('Files') }}
|
||||
</a></li>
|
||||
{% endif %}
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('wiki_help', anchor='training') }}">
|
||||
<span class="icon icon-help"></span> {{ _('Help') }}
|
||||
</a></li>
|
||||
<li class="menu__seperator"></li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="section__body typo">
|
||||
<dl class="large horizontal">
|
||||
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
|
||||
<dt>{{ _('Status') }}</dt><dd>{% if tsdoc['enroll'] %}{{ _('Completed' if tsdoc['done'] else 'In Progress') }}{% else %}{{ _('Not Enrolled') }}{% endif %}</dd>
|
||||
{% endif %}
|
||||
<dt>{{ _('Enrollees') }}</dt><dd>{{ tdoc.attend|default(0) }}</dd>
|
||||
<dt>{{ _('Created By') }}</dt>
|
||||
<dd>{{ user.render_inline(udoc, badge=false) }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -24,8 +24,8 @@
|
||||
</label>
|
||||
</div></div>
|
||||
<div class="row"><div class="columns">
|
||||
<label class="inverse material checkbox">
|
||||
<input type="checkbox" name="rememberme">{{ _('Remember me') }}
|
||||
<label class="inverse material checkbox" style="color:#fff;">
|
||||
<input type="checkbox" name="rememberme" class="checkbox">{{ _('Remember me') }}
|
||||
</label>
|
||||
</div></div>
|
||||
<div class="row"><div class="columns">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/vjudge",
|
||||
"version": "1.7.1",
|
||||
"version": "1.7.2",
|
||||
"description": "Submit problems to remote oj",
|
||||
"main": "package.json",
|
||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||
|
||||
Reference in new issue
Block a user