mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-19 17:38:19 +08:00
core&ui: add contest_problemlist page
This commit is contained in:
1 parent
a364519a2e
commit
a43dc22599
12 files changed
+291
-169
No files matched your search
@@ -8,6 +8,7 @@ contest_detail: Contest Detail
|
|||||||
contest_edit: Contest Edit
|
contest_edit: Contest Edit
|
||||||
contest_files: Contest Files
|
contest_files: Contest Files
|
||||||
contest_main: Contest
|
contest_main: Contest
|
||||||
|
contest_problemlist: Proble List
|
||||||
contest_scoreboard: Scoreboard
|
contest_scoreboard: Scoreboard
|
||||||
discussion_create: Create Discussion
|
discussion_create: Create Discussion
|
||||||
discussion_detail: Discussion Detail
|
discussion_detail: Discussion Detail
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ Attended: 已参加
|
|||||||
author: 作者
|
author: 作者
|
||||||
Auto format code: 自动格式化代码
|
Auto format code: 自动格式化代码
|
||||||
Avatar: 头像
|
Avatar: 头像
|
||||||
|
Back to contest list: 返回比赛列表
|
||||||
Be Copied: 被复制
|
Be Copied: 被复制
|
||||||
Begin at: 开始于
|
Begin at: 开始于
|
||||||
Begin Date: 开始日期
|
Begin Date: 开始日期
|
||||||
@@ -159,6 +160,7 @@ contest_detail: 比赛详情
|
|||||||
contest_edit: 编辑比赛
|
contest_edit: 编辑比赛
|
||||||
contest_files: 比赛文件
|
contest_files: 比赛文件
|
||||||
contest_main: 比赛
|
contest_main: 比赛
|
||||||
|
contest_problemlist: 题目列表
|
||||||
contest_scoreboard: 比赛成绩表
|
contest_scoreboard: 比赛成绩表
|
||||||
Contest: 比赛
|
Contest: 比赛
|
||||||
Contests per page: 每页展示的比赛数量
|
Contests per page: 每页展示的比赛数量
|
||||||
@@ -437,8 +439,8 @@ List View: 列表视图
|
|||||||
Live: 正在进行
|
Live: 正在进行
|
||||||
Live...: 正在进行…
|
Live...: 正在进行…
|
||||||
Location: 位置
|
Location: 位置
|
||||||
Lock Topic: 锁定主题
|
|
||||||
Lock discussions: 锁定讨论
|
Lock discussions: 锁定讨论
|
||||||
|
Lock Topic: 锁定主题
|
||||||
Login to Attend Contest: 登录后参加比赛
|
Login to Attend Contest: 登录后参加比赛
|
||||||
Login to Claim Homework: 登录后认领作业
|
Login to Claim Homework: 登录后认领作业
|
||||||
Login to Create a Discussion: 登录后创建讨论
|
Login to Create a Discussion: 登录后创建讨论
|
||||||
@@ -868,8 +870,8 @@ View or create discussion: 查看与发表讨论
|
|||||||
View or submit solution: 查看与发表题解
|
View or submit solution: 查看与发表题解
|
||||||
View Owned Problems: 查看自己的题目
|
View Owned Problems: 查看自己的题目
|
||||||
View Owned Training Plans: 查看自己的训练计划
|
View Owned Training Plans: 查看自己的训练计划
|
||||||
View problem solutions: 查看题解
|
|
||||||
View problem solutions after accept: 题目通过后查看题解
|
View problem solutions after accept: 题目通过后查看题解
|
||||||
|
View problem solutions: 查看题解
|
||||||
View Problem: 查看题目
|
View Problem: 查看题目
|
||||||
View problems: 查看题目
|
View problems: 查看题目
|
||||||
View ranking: 查看排名
|
View ranking: 查看排名
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { ObjectID } from 'mongodb';
|
|||||||
import { Counter, sortFiles, Time } from '@hydrooj/utils/lib/utils';
|
import { Counter, sortFiles, Time } from '@hydrooj/utils/lib/utils';
|
||||||
import {
|
import {
|
||||||
BadRequestError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError,
|
BadRequestError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError,
|
||||||
ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError, InvalidTokenError,
|
ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError, ForbiddenError,
|
||||||
NotAssignedError, PermissionError, ValidationError,
|
InvalidTokenError, NotAssignedError, PermissionError, ValidationError,
|
||||||
} from '../error';
|
} from '../error';
|
||||||
import { Tdoc } from '../interface';
|
import { Tdoc } from '../interface';
|
||||||
import paginate from '../lib/paginate';
|
import paginate from '../lib/paginate';
|
||||||
@@ -92,6 +92,10 @@ export class ContestDetailBaseHandler extends Handler {
|
|||||||
|
|
||||||
@param('tid', Types.ObjectID, true)
|
@param('tid', Types.ObjectID, true)
|
||||||
async __prepare(domainId: string, tid: ObjectID) {
|
async __prepare(domainId: string, tid: ObjectID) {
|
||||||
|
if (this.session.tid && this.session.tid !== tid) {
|
||||||
|
// TODO: you are already in a contest
|
||||||
|
throw new ForbiddenError();
|
||||||
|
}
|
||||||
if (!tid) return; // ProblemDetailHandler also extends from ContestDetailBaseHandler
|
if (!tid) return; // ProblemDetailHandler also extends from ContestDetailBaseHandler
|
||||||
[this.tdoc, this.tsdoc] = await Promise.all([
|
[this.tdoc, this.tsdoc] = await Promise.all([
|
||||||
contest.get(domainId, tid),
|
contest.get(domainId, tid),
|
||||||
@@ -110,32 +114,33 @@ export class ContestDetailBaseHandler extends Handler {
|
|||||||
|
|
||||||
@param('tid', Types.ObjectID, true)
|
@param('tid', Types.ObjectID, true)
|
||||||
async after(domainId: string, tid: ObjectID) {
|
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;
|
if (this.request.json || !this.response.template) return;
|
||||||
const pdoc = 'pdoc' in this ? (this as any).pdoc : {};
|
const pdoc = 'pdoc' in this ? (this as any).pdoc : {};
|
||||||
this.response.body.overrideNav = [
|
this.response.body.overrideNav = [
|
||||||
{ name: 'homepage', args: { prefix: 'homepage' }, checker: () => true },
|
{
|
||||||
|
name: 'contest_list',
|
||||||
|
args: {},
|
||||||
|
displayName: 'Back to contest list',
|
||||||
|
checker: () => !(this.tdoc.access & contest.AccessControl.LOCK_OTHER),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'contest_detail',
|
name: 'contest_detail',
|
||||||
displayName: this.tdoc.title,
|
displayName: this.tdoc.title,
|
||||||
args: { tid, prefix: 'contest_detail' },
|
args: { tid, prefix: 'contest_detail' },
|
||||||
checker: () => true,
|
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',
|
name: 'contest_scoreboard',
|
||||||
args: { tid, prefix: 'contest_scoreboard' },
|
args: { tid, prefix: 'contest_scoreboard' },
|
||||||
checker: () => contest.canShowScoreboard.call(this, this.tdoc, true),
|
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}`,
|
displayName: `${String.fromCharCode(65 + this.tdoc.pids.indexOf(pdoc.docId))}. ${pdoc.title}`,
|
||||||
args: { query: { tid }, pid: pdoc.docId, prefix: 'contest_detail_problem' },
|
args: { query: { tid }, pid: pdoc.docId, prefix: 'contest_detail_problem' },
|
||||||
@@ -145,10 +150,30 @@ export class ContestDetailBaseHandler extends Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ContestDetailHandler extends ContestDetailBaseHandler {
|
export class ContestDetailHandler extends Handler {
|
||||||
|
tdoc?: Tdoc<30>;
|
||||||
|
tsdoc?: any;
|
||||||
|
|
||||||
@param('tid', Types.ObjectID)
|
@param('tid', Types.ObjectID)
|
||||||
async prepare(domainId: string, tid: 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 (contest.RULES[this.tdoc.rule].hidden) throw new ContestNotFoundError(domainId, tid);
|
||||||
|
if (this.session.tid && this.session.tid !== tid) {
|
||||||
|
// TODO: you are already in a contest
|
||||||
|
throw new ForbiddenError();
|
||||||
|
}
|
||||||
|
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)
|
@param('tid', Types.ObjectID)
|
||||||
@@ -158,32 +183,41 @@ export class ContestDetailHandler extends ContestDetailBaseHandler {
|
|||||||
this.response.body = {
|
this.response.body = {
|
||||||
tdoc: this.tdoc, tsdoc: pick(this.tsdoc, ['attend', 'startAt']), udict,
|
tdoc: this.tdoc, tsdoc: pick(this.tsdoc, ['attend', 'startAt']), udict,
|
||||||
};
|
};
|
||||||
if (!this.request.json) {
|
if (this.request.json) return;
|
||||||
this.response.body.tdoc.content = this.response.body.tdoc.content
|
this.response.body.tdoc.content = this.response.body.tdoc.content
|
||||||
.replace(/\(file:\/\//g, `(./${this.tdoc.docId}/file/`)
|
.replace(/\(file:\/\//g, `(./${this.tdoc.docId}/file/`)
|
||||||
.replace(/="file:\/\//g, `="./${this.tdoc.docId}/file/`);
|
.replace(/="file:\/\//g, `="./${this.tdoc.docId}/file/`);
|
||||||
}
|
const pdoc = 'pdoc' in this ? (this as any).pdoc : {};
|
||||||
if (
|
this.response.body.overrideNav = [
|
||||||
(contest.isNotStarted(this.tdoc) || (!this.tsdoc?.attend && !contest.isDone(this.tdoc)))
|
{
|
||||||
&& !this.user.own(this.tdoc)
|
name: 'contest_list',
|
||||||
&& !this.user.hasPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD)
|
args: {},
|
||||||
) return;
|
displayName: 'Back to contest list',
|
||||||
const pdict = await problem.getList(domainId, this.tdoc.pids, true, true, problem.PROJECTION_CONTEST_LIST);
|
checker: () => !(this.tdoc.access & contest.AccessControl.LOCK_OTHER),
|
||||||
let psdict = {};
|
},
|
||||||
let rdict = {};
|
{
|
||||||
if (this.tsdoc) {
|
name: 'contest_detail',
|
||||||
if (this.tsdoc.attend && !this.tsdoc.startAt && contest.isOngoing(this.tdoc)) {
|
displayName: this.tdoc.title,
|
||||||
await contest.setStatus(domainId, tid, this.user._id, { startAt: new Date() });
|
args: { tid, prefix: 'contest_detail' },
|
||||||
this.tsdoc.startAt = new Date();
|
checker: () => true,
|
||||||
}
|
},
|
||||||
psdict = this.tsdoc.detail || {};
|
{
|
||||||
if (contest.canShowSelfRecord.call(this, this.tdoc)) {
|
name: 'contest_problemlist',
|
||||||
rdict = await record.getList(domainId, Object.values(psdict).map((i: any) => i.rid));
|
args: { tid },
|
||||||
} else {
|
checker: () => contest.isDone(this.tdoc, this.tsdoc)
|
||||||
for (const i in psdict) rdict[psdict[i].rid] = { _id: psdict[i].rid };
|
|| (contest.isOngoing(this.tdoc, this.tsdoc) && this.tsdoc?.attend),
|
||||||
}
|
},
|
||||||
}
|
{
|
||||||
Object.assign(this.response.body, { pdict, psdict, rdict });
|
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)
|
@param('tid', Types.ObjectID)
|
||||||
@@ -194,16 +228,35 @@ export class ContestDetailHandler extends ContestDetailBaseHandler {
|
|||||||
await contest.attend(domainId, tid, this.user._id);
|
await contest.attend(domainId, tid, this.user._id);
|
||||||
this.back();
|
this.back();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ContestProblemListHandler extends ContestDetailBaseHandler {
|
||||||
@param('tid', Types.ObjectID)
|
@param('tid', Types.ObjectID)
|
||||||
async postDelete(domainId: string, tid: ObjectID) {
|
async get(domainId: string, tid: ObjectID) {
|
||||||
if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST);
|
if (contest.isNotStarted(this.tdoc) || (!this.tsdoc?.attend && !contest.isDone(this.tdoc))) throw new ForbiddenError();
|
||||||
await contest.del(domainId, tid);
|
const [pdict, udict] = await Promise.all([
|
||||||
await record.updateMulti(domainId, { domainId, contest: tid }, undefined, undefined, { contest: '' });
|
problem.getList(domainId, this.tdoc.pids, true, true, problem.PROJECTION_CONTEST_LIST),
|
||||||
await ScheduleModel.deleteMany({
|
user.getList(domainId, [this.tdoc.owner, this.user._id]),
|
||||||
type: 'schedule', subType: 'contest', domainId, tid,
|
]);
|
||||||
});
|
this.response.body = {
|
||||||
this.response.redirect = this.url('contest_main');
|
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 }).toArray(),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
for (const i of psdocs) this.response.body.rdict[i.rid] = { _id: i.rid };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,7 +411,7 @@ export class ContestEditHandler extends Handler {
|
|||||||
@param('assign', Types.CommaSeperatedArray, true)
|
@param('assign', Types.CommaSeperatedArray, true)
|
||||||
@param('lock', Types.UnsignedInt, true)
|
@param('lock', Types.UnsignedInt, true)
|
||||||
@param('contestDuration', Types.Float, true)
|
@param('contestDuration', Types.Float, true)
|
||||||
async post(
|
async postUpdate(
|
||||||
domainId: string, tid: ObjectID, beginAtDate: string, beginAtTime: string, duration: number,
|
domainId: string, tid: ObjectID, beginAtDate: string, beginAtTime: string, duration: number,
|
||||||
title: string, content: string, rule: string, _pids: string, rated = false,
|
title: string, content: string, rule: string, _pids: string, rated = false,
|
||||||
_code = '', autoHide = false, assign: string[] = null, lock: number = null,
|
_code = '', autoHide = false, assign: string[] = null, lock: number = null,
|
||||||
@@ -408,6 +461,17 @@ export class ContestEditHandler extends Handler {
|
|||||||
this.response.body = { tid };
|
this.response.body = { tid };
|
||||||
this.response.redirect = this.url('contest_detail', { 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 {
|
export class ContestCodeHandler extends Handler {
|
||||||
@@ -526,6 +590,7 @@ export async function apply(ctx) {
|
|||||||
ctx.Route('contest_create', '/contest/create', ContestEditHandler);
|
ctx.Route('contest_create', '/contest/create', ContestEditHandler);
|
||||||
ctx.Route('contest_main', '/contest', ContestListHandler, PERM.PERM_VIEW_CONTEST);
|
ctx.Route('contest_main', '/contest', ContestListHandler, PERM.PERM_VIEW_CONTEST);
|
||||||
ctx.Route('contest_detail', '/contest/:tid', ContestDetailHandler, PERM.PERM_VIEW_CONTEST);
|
ctx.Route('contest_detail', '/contest/:tid', ContestDetailHandler, PERM.PERM_VIEW_CONTEST);
|
||||||
|
ctx.Route('contest_problemlist', '/contest/:tid/problems', ContestProblemListHandler, PERM.PERM_VIEW_CONTEST);
|
||||||
ctx.Route('contest_broadcast', '/contest/:tid/broadcast', ContestBroadcastHandler);
|
ctx.Route('contest_broadcast', '/contest/:tid/broadcast', ContestBroadcastHandler);
|
||||||
ctx.Route('contest_edit', '/contest/:tid/edit', ContestEditHandler, PERM.PERM_VIEW_CONTEST);
|
ctx.Route('contest_edit', '/contest/:tid/edit', ContestEditHandler, PERM.PERM_VIEW_CONTEST);
|
||||||
ctx.Route('contest_scoreboard', '/contest/:tid/scoreboard', ContestScoreboardHandler, PERM.PERM_VIEW_CONTEST_SCOREBOARD);
|
ctx.Route('contest_scoreboard', '/contest/:tid/scoreboard', ContestScoreboardHandler, PERM.PERM_VIEW_CONTEST_SCOREBOARD);
|
||||||
|
|||||||
@@ -366,6 +366,8 @@ export interface Tdoc<docType = document['TYPE_CONTEST'] | document['TYPE_TRAINI
|
|||||||
// For contest
|
// For contest
|
||||||
lockAt?: Date;
|
lockAt?: Date;
|
||||||
unlocked?: boolean;
|
unlocked?: boolean;
|
||||||
|
access?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In hours
|
* In hours
|
||||||
* 在比赛有效时间内选择特定的 X 小时参加比赛(从首次打开比赛算起)
|
* 在比赛有效时间内选择特定的 X 小时参加比赛(从首次打开比赛算起)
|
||||||
|
|||||||
@@ -510,6 +510,13 @@ export const RULES: ContestRules = {
|
|||||||
acm, oi, homework, ioi,
|
acm, oi, homework, ioi,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum AccessControl {
|
||||||
|
FREE,
|
||||||
|
LOCK_OTHER = 2,
|
||||||
|
UNIQUE_LOGIN = 4,
|
||||||
|
CLIENT_REQUIRED = 8,
|
||||||
|
}
|
||||||
|
|
||||||
function _getStatusJournal(tsdoc) {
|
function _getStatusJournal(tsdoc) {
|
||||||
return tsdoc.journal.sort((a, b) => (a.rid.generationTime - b.rid.generationTime));
|
return tsdoc.journal.sort((a, b) => (a.rid.generationTime - b.rid.generationTime));
|
||||||
}
|
}
|
||||||
|
|||||||
+60
@@ -107,3 +107,63 @@ $highlight-button-color = #F6DF45
|
|||||||
|
|
||||||
.detail-button
|
.detail-button
|
||||||
width: auto
|
width: auto
|
||||||
|
|
||||||
|
|
||||||
|
.page--contest_edit
|
||||||
|
.page--contest_create
|
||||||
|
label
|
||||||
|
&.checkbox
|
||||||
|
margin-top: 24px
|
||||||
|
|
||||||
|
|
||||||
|
.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,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
|
|
||||||
@@ -37,66 +37,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
<div class="medium-3 columns">
|
<div class="medium-3 columns">
|
||||||
{% set owner_udoc = udict[tdoc.owner] %}
|
{% set owner_udoc = udict[tdoc.owner] %}
|
||||||
|
|||||||
@@ -108,7 +108,7 @@
|
|||||||
<div class="columns">
|
<div class="columns">
|
||||||
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
|
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
|
||||||
{% if page_name == 'contest_edit' %}
|
{% if page_name == 'contest_edit' %}
|
||||||
<button type="submit" class="rounded primary button">
|
<button name="operation" value="update" type="submit" class="rounded primary button">
|
||||||
{{ _('Update') }}
|
{{ _('Update') }}
|
||||||
</button>
|
</button>
|
||||||
{% if handler.user.own(tdoc) or handler.user.hasPerm(perm.PERM_EDIT_CONTEST) %}
|
{% if handler.user.own(tdoc) or handler.user.hasPerm(perm.PERM_EDIT_CONTEST) %}
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<button type="submit" class="rounded primary button">
|
<button name="operation" value="update" type="submit" class="rounded primary button">
|
||||||
{{ _('Create') }}
|
{{ _('Create') }}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
{% 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">
|
||||||
|
{{ problem.render_problem_title(pdict[pid], tdoc=tdoc, show_invisible_flag=false, show_tags=false) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{%- endfor -%}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<div class="section__body no-padding">
|
||||||
|
{% if not rdocs.length %}
|
||||||
|
{{ nothing.render('Oh, there is no task in the queue!') }}
|
||||||
|
{% 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 %}
|
||||||
Reference in new issue
Block a user