mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 09:46:28 +08:00
Merge pull request #442 from hydro-dev/contest-problemlist
This commit is contained in:
17 files changed
+204
-64
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydrooj",
|
"name": "hydrooj",
|
||||||
"version": "4.1.1",
|
"version": "4.2.0",
|
||||||
"bin": "bin/hydrooj.js",
|
"bin": "bin/hydrooj.js",
|
||||||
"main": "src/plugin-api",
|
"main": "src/plugin-api",
|
||||||
"module": "src/plugin-api",
|
"module": "src/plugin-api",
|
||||||
|
|||||||
@@ -127,7 +127,12 @@ export class ContestDetailBaseHandler extends Handler {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'record_main',
|
name: 'record_main',
|
||||||
args: { tid, prefix: 'record_main', query: contest.canShowRecord.call(this, this.tdoc, true) ? {} : { uidOrName: this.user._id } },
|
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),
|
checker: () => contest.canShowSelfRecord.call(this, this.tdoc, true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -163,14 +168,14 @@ export class ContestDetailHandler extends ContestDetailBaseHandler {
|
|||||||
&& !this.user.hasPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD)
|
&& !this.user.hasPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD)
|
||||||
) return;
|
) return;
|
||||||
const pdict = await problem.getList(domainId, this.tdoc.pids, true, undefined, undefined, problem.PROJECTION_CONTEST_LIST);
|
const pdict = await problem.getList(domainId, this.tdoc.pids, true, undefined, undefined, problem.PROJECTION_CONTEST_LIST);
|
||||||
const psdict = {};
|
let psdict = {};
|
||||||
let rdict = {};
|
let rdict = {};
|
||||||
if (this.tsdoc) {
|
if (this.tsdoc) {
|
||||||
if (this.tsdoc.attend && !this.tsdoc.startAt && contest.isOngoing(this.tdoc)) {
|
if (this.tsdoc.attend && !this.tsdoc.startAt && contest.isOngoing(this.tdoc)) {
|
||||||
await contest.setStatus(domainId, tid, this.user._id, { startAt: new Date() });
|
await contest.setStatus(domainId, tid, this.user._id, { startAt: new Date() });
|
||||||
this.tsdoc.startAt = new Date();
|
this.tsdoc.startAt = new Date();
|
||||||
}
|
}
|
||||||
for (const pdetail of this.tsdoc.journal || []) psdict[pdetail.pid] = pdetail;
|
psdict = this.tsdoc.detail || {};
|
||||||
if (contest.canShowSelfRecord.call(this, this.tdoc)) {
|
if (contest.canShowSelfRecord.call(this, this.tdoc)) {
|
||||||
rdict = await record.getList(domainId, Object.values(psdict).map((i: any) => i.rid));
|
rdict = await record.getList(domainId, Object.values(psdict).map((i: any) => i.rid));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -377,8 +377,12 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler {
|
|||||||
solutionCount: scnt,
|
solutionCount: scnt,
|
||||||
discussionCount: dcnt,
|
discussionCount: dcnt,
|
||||||
tdoc: this.tdoc,
|
tdoc: this.tdoc,
|
||||||
tsdoc: pick(this.tsdoc, ['attend', 'startAt']),
|
|
||||||
};
|
};
|
||||||
|
if (this.tdoc && this.tsdoc) {
|
||||||
|
const fields = ['attend', 'startAt'];
|
||||||
|
if (contest.canShowSelfRecord.call(this, this.tdoc, true)) fields.push('detail');
|
||||||
|
this.response.body.tsdoc = pick(this.tsdoc, fields);
|
||||||
|
}
|
||||||
this.response.template = 'problem_detail.html';
|
this.response.template = 'problem_detail.html';
|
||||||
this.UiContext.extraTitleContent = this.pdoc.title;
|
this.UiContext.extraTitleContent = this.pdoc.title;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,13 +15,14 @@ import * as system from '../model/system';
|
|||||||
import TaskModel from '../model/task';
|
import TaskModel from '../model/task';
|
||||||
import user from '../model/user';
|
import user from '../model/user';
|
||||||
import * as bus from '../service/bus';
|
import * as bus from '../service/bus';
|
||||||
import {
|
import { ConnectionHandler, param, Types } from '../service/server';
|
||||||
ConnectionHandler, Handler, param, Types,
|
|
||||||
} from '../service/server';
|
|
||||||
import { buildProjection } from '../utils';
|
import { buildProjection } from '../utils';
|
||||||
|
import { ContestDetailBaseHandler } from './contest';
|
||||||
import { postJudge } from './judge';
|
import { postJudge } from './judge';
|
||||||
|
|
||||||
class RecordListHandler extends Handler {
|
class RecordListHandler extends ContestDetailBaseHandler {
|
||||||
|
tdoc?: Tdoc<30>;
|
||||||
|
|
||||||
@param('page', Types.PositiveInt, true)
|
@param('page', Types.PositiveInt, true)
|
||||||
@param('pid', Types.Name, true)
|
@param('pid', Types.Name, true)
|
||||||
@param('tid', Types.ObjectID, true)
|
@param('tid', Types.ObjectID, true)
|
||||||
@@ -42,6 +43,7 @@ class RecordListHandler extends Handler {
|
|||||||
if (full) uidOrName = this.user._id.toString();
|
if (full) uidOrName = this.user._id.toString();
|
||||||
if (tid) {
|
if (tid) {
|
||||||
tdoc = await contest.get(domainId, tid);
|
tdoc = await contest.get(domainId, tid);
|
||||||
|
this.tdoc = tdoc;
|
||||||
if (!tdoc) throw new ContestNotFoundError(domainId, pid);
|
if (!tdoc) throw new ContestNotFoundError(domainId, pid);
|
||||||
if (!contest.canShowScoreboard.call(this, tdoc, true)) throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
|
if (!contest.canShowScoreboard.call(this, tdoc, true)) throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
|
||||||
if (!this.user.own(tdoc) && !(await contest.getStatus(domainId, tid, this.user._id))?.attend) {
|
if (!this.user.own(tdoc) && !(await contest.getStatus(domainId, tid, this.user._id))?.attend) {
|
||||||
@@ -103,8 +105,10 @@ class RecordListHandler extends Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RecordDetailHandler extends Handler {
|
class RecordDetailHandler extends ContestDetailBaseHandler {
|
||||||
rdoc: RecordDoc;
|
rdoc: RecordDoc;
|
||||||
|
tdoc?: Tdoc<30>;
|
||||||
|
|
||||||
@param('rid', Types.ObjectID)
|
@param('rid', Types.ObjectID)
|
||||||
async prepare(domainId: string, rid: ObjectID) {
|
async prepare(domainId: string, rid: ObjectID) {
|
||||||
this.rdoc = await record.get(domainId, rid);
|
this.rdoc = await record.get(domainId, rid);
|
||||||
@@ -130,15 +134,15 @@ class RecordDetailHandler extends Handler {
|
|||||||
// eslint-disable-next-line consistent-return
|
// eslint-disable-next-line consistent-return
|
||||||
async get(domainId: string, rid: ObjectID, download = false) {
|
async get(domainId: string, rid: ObjectID, download = false) {
|
||||||
const rdoc = this.rdoc;
|
const rdoc = this.rdoc;
|
||||||
let tdoc: Tdoc<30>;
|
|
||||||
if (rdoc.contest?.toString() === '000000000000000000000000') {
|
if (rdoc.contest?.toString() === '000000000000000000000000') {
|
||||||
if (rdoc.uid !== this.user._id) throw new PermissionError(PERM.PERM_READ_RECORD_CODE);
|
if (rdoc.uid !== this.user._id) throw new PermissionError(PERM.PERM_READ_RECORD_CODE);
|
||||||
} else if (rdoc.contest) {
|
} else if (rdoc.contest) {
|
||||||
tdoc = await contest.get(domainId, rdoc.contest);
|
this.tdoc = await contest.get(domainId, rdoc.contest);
|
||||||
let canView = this.user.own(tdoc);
|
let canView = this.user.own(this.tdoc);
|
||||||
canView ||= contest.canShowRecord.call(this, tdoc);
|
canView ||= contest.canShowRecord.call(this, this.tdoc);
|
||||||
canView ||= contest.canShowSelfRecord.call(this, tdoc, true) && rdoc.uid === this.user._id;
|
canView ||= contest.canShowSelfRecord.call(this, this.tdoc, true) && rdoc.uid === this.user._id;
|
||||||
if (!canView) throw new PermissionError(rid);
|
if (!canView) throw new PermissionError(rid);
|
||||||
|
this.args.tid = this.tdoc.docId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line prefer-const
|
// eslint-disable-next-line prefer-const
|
||||||
@@ -152,8 +156,8 @@ class RecordDetailHandler extends Handler {
|
|||||||
canViewCode ||= this.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE);
|
canViewCode ||= this.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE);
|
||||||
canViewCode ||= this.user.hasPerm(PERM.PERM_READ_RECORD_CODE);
|
canViewCode ||= this.user.hasPerm(PERM.PERM_READ_RECORD_CODE);
|
||||||
canViewCode ||= this.user.hasPerm(PERM.PERM_READ_RECORD_CODE_ACCEPT) && self?.status === STATUS.STATUS_ACCEPTED;
|
canViewCode ||= this.user.hasPerm(PERM.PERM_READ_RECORD_CODE_ACCEPT) && self?.status === STATUS.STATUS_ACCEPTED;
|
||||||
if (tdoc && tdoc.allowViewCode && contest.isDone(tdoc)) {
|
if (this.tdoc && this.tdoc.allowViewCode && contest.isDone(this.tdoc)) {
|
||||||
const tsdoc = await contest.getStatus(domainId, tdoc.docId, this.user._id);
|
const tsdoc = await contest.getStatus(domainId, this.tdoc.docId, this.user._id);
|
||||||
canViewCode ||= tsdoc?.attend;
|
canViewCode ||= tsdoc?.attend;
|
||||||
}
|
}
|
||||||
if (!canViewCode) {
|
if (!canViewCode) {
|
||||||
@@ -167,7 +171,7 @@ class RecordDetailHandler extends Handler {
|
|||||||
|
|
||||||
this.response.template = 'record_detail.html';
|
this.response.template = 'record_detail.html';
|
||||||
this.response.body = {
|
this.response.body = {
|
||||||
udoc, rdoc, pdoc, tdoc,
|
udoc, rdoc, pdoc, tdoc: this.tdoc,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -483,7 +483,7 @@ export interface OplogDoc extends Record<string, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ContestStat extends Record<string, any> {
|
export interface ContestStat extends Record<string, any> {
|
||||||
detail: any,
|
detail: Record<number, Record<string, any>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContestRule<T = any> {
|
export interface ContestRule<T = any> {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const acm = buildContestRule({
|
|||||||
stat(tdoc, journal: AcmJournal[], ignoreLock = false) {
|
stat(tdoc, journal: AcmJournal[], ignoreLock = false) {
|
||||||
const naccept = Counter<number>();
|
const naccept = Counter<number>();
|
||||||
const effective: Record<number, AcmJournal> = {};
|
const effective: Record<number, AcmJournal> = {};
|
||||||
const detail: AcmDetail[] = [];
|
const detail: Record<number, AcmDetail> = {};
|
||||||
let accept = 0;
|
let accept = 0;
|
||||||
let time = 0;
|
let time = 0;
|
||||||
for (const j of filterEffective(tdoc, journal, ignoreLock)) {
|
for (const j of filterEffective(tdoc, journal, ignoreLock)) {
|
||||||
@@ -77,11 +77,11 @@ const acm = buildContestRule({
|
|||||||
const j = effective[pid];
|
const j = effective[pid];
|
||||||
const real = j.rid.generationTime - Math.floor(tdoc.beginAt.getTime() / 1000);
|
const real = j.rid.generationTime - Math.floor(tdoc.beginAt.getTime() / 1000);
|
||||||
const penalty = 20 * 60 * naccept[j.pid];
|
const penalty = 20 * 60 * naccept[j.pid];
|
||||||
detail.push({
|
detail[pid] = {
|
||||||
...j, naccept: naccept[j.pid], time: real + penalty, real, penalty,
|
...j, naccept: naccept[j.pid], time: real + penalty, real, penalty,
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
for (const d of detail.filter((i) => i.status === STATUS.STATUS_ACCEPTED)) {
|
for (const d of Object.values(detail).filter((i) => i.status === STATUS.STATUS_ACCEPTED)) {
|
||||||
accept++;
|
accept++;
|
||||||
time += d.time;
|
time += d.time;
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,12 @@ const acm = buildContestRule({
|
|||||||
{ type: 'rank', value: '#' },
|
{ type: 'rank', value: '#' },
|
||||||
{ type: 'user', value: _('User') },
|
{ type: 'user', value: _('User') },
|
||||||
];
|
];
|
||||||
if (isExport) columns.push({ type: 'email', value: _('Email') });
|
if (isExport) {
|
||||||
|
columns.push({ type: 'email', value: _('Email') });
|
||||||
|
columns.push({ type: 'string', value: _('School') });
|
||||||
|
columns.push({ type: 'string', value: _('Name') });
|
||||||
|
columns.push({ type: 'string', value: _('Student ID') });
|
||||||
|
}
|
||||||
columns.push({ type: 'solved', value: `${_('Solved')}\n${_('Total Time')}` });
|
columns.push({ type: 'solved', value: `${_('Solved')}\n${_('Total Time')}` });
|
||||||
for (let i = 1; i <= tdoc.pids.length; i++) {
|
for (let i = 1; i <= tdoc.pids.length; i++) {
|
||||||
const pid = tdoc.pids[i - 1];
|
const pid = tdoc.pids[i - 1];
|
||||||
@@ -118,13 +123,17 @@ const acm = buildContestRule({
|
|||||||
return columns;
|
return columns;
|
||||||
},
|
},
|
||||||
async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc, meta) {
|
async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc, meta) {
|
||||||
const tsddict: Record<number, AcmDetail> = {};
|
const tsddict = tsdoc.detail || {};
|
||||||
for (const item of tsdoc.detail || []) tsddict[item.pid] = item;
|
|
||||||
const row: ScoreboardRow = [
|
const row: ScoreboardRow = [
|
||||||
{ type: 'rank', value: rank.toString() },
|
{ type: 'rank', value: rank.toString() },
|
||||||
{ type: 'user', value: udoc.uname, raw: tsdoc.uid },
|
{ type: 'user', value: udoc.uname, raw: tsdoc.uid },
|
||||||
];
|
];
|
||||||
if (isExport) row.push({ type: 'email', value: udoc.mail });
|
if (isExport) {
|
||||||
|
row.push({ type: 'email', value: udoc.mail });
|
||||||
|
row.push({ type: 'string', value: udoc.school });
|
||||||
|
row.push({ type: 'string', value: udoc.displayName });
|
||||||
|
row.push({ type: 'string', value: udoc.studentId });
|
||||||
|
}
|
||||||
row.push({
|
row.push({
|
||||||
type: 'time',
|
type: 'time',
|
||||||
value: `${tsdoc.accept || 0}\n${formatSeconds(tsdoc.time || 0.0, false)}`,
|
value: `${tsdoc.accept || 0}\n${formatSeconds(tsdoc.time || 0.0, false)}`,
|
||||||
@@ -220,7 +229,12 @@ const oi = buildContestRule({
|
|||||||
{ type: 'rank', value: '#' },
|
{ type: 'rank', value: '#' },
|
||||||
{ type: 'user', value: _('User') },
|
{ type: 'user', value: _('User') },
|
||||||
];
|
];
|
||||||
if (isExport) columns.push({ type: 'email', value: _('Email') });
|
if (isExport) {
|
||||||
|
columns.push({ type: 'email', value: _('Email') });
|
||||||
|
columns.push({ type: 'string', value: _('School') });
|
||||||
|
columns.push({ type: 'string', value: _('Name') });
|
||||||
|
columns.push({ type: 'string', value: _('Student ID') });
|
||||||
|
}
|
||||||
columns.push({ type: 'total_score', value: _('Total Score') });
|
columns.push({ type: 'total_score', value: _('Total Score') });
|
||||||
for (let i = 1; i <= tdoc.pids.length; i++) {
|
for (let i = 1; i <= tdoc.pids.length; i++) {
|
||||||
if (isExport) {
|
if (isExport) {
|
||||||
@@ -244,7 +258,12 @@ const oi = buildContestRule({
|
|||||||
{ type: 'rank', value: rank.toString() },
|
{ type: 'rank', value: rank.toString() },
|
||||||
{ type: 'user', value: udoc.uname, raw: tsdoc.uid },
|
{ type: 'user', value: udoc.uname, raw: tsdoc.uid },
|
||||||
];
|
];
|
||||||
if (isExport) row.push({ type: 'email', value: udoc.mail });
|
if (isExport) {
|
||||||
|
row.push({ type: 'email', value: udoc.mail });
|
||||||
|
row.push({ type: 'string', value: udoc.school });
|
||||||
|
row.push({ type: 'string', value: udoc.displayName });
|
||||||
|
row.push({ type: 'string', value: udoc.studentId });
|
||||||
|
}
|
||||||
row.push({ type: 'total_score', value: tsdoc.score || 0 });
|
row.push({ type: 'total_score', value: tsdoc.score || 0 });
|
||||||
for (const pid of tdoc.pids) {
|
for (const pid of tdoc.pids) {
|
||||||
const index = `${tsdoc.uid}/${tdoc.domainId}/${pid}`;
|
const index = `${tsdoc.uid}/${tdoc.domainId}/${pid}`;
|
||||||
@@ -366,7 +385,7 @@ const homework = buildContestRule({
|
|||||||
score: sumBy(detail, 'score'),
|
score: sumBy(detail, 'score'),
|
||||||
penaltyScore: sumBy(detail, 'penaltyScore'),
|
penaltyScore: sumBy(detail, 'penaltyScore'),
|
||||||
time: Math.sum(detail.map((d) => d.time)),
|
time: Math.sum(detail.map((d) => d.time)),
|
||||||
detail,
|
detail: effective,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
showScoreboard: () => true,
|
showScoreboard: () => true,
|
||||||
@@ -410,10 +429,7 @@ const homework = buildContestRule({
|
|||||||
return columns;
|
return columns;
|
||||||
},
|
},
|
||||||
async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc) {
|
async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc) {
|
||||||
const tsddict = {};
|
const tsddict = tsdoc.detail;
|
||||||
for (const item of tsdoc.detail || []) {
|
|
||||||
if (!tsddict[item.pid] || tsddict[item.pid].score <= item.score) tsddict[item.pid] = item;
|
|
||||||
}
|
|
||||||
const row: ScoreboardRow = [
|
const row: ScoreboardRow = [
|
||||||
{ type: 'rank', value: rank.toString() },
|
{ type: 'rank', value: rank.toString() },
|
||||||
{
|
{
|
||||||
@@ -484,7 +500,7 @@ function _getStatusJournal(tsdoc) {
|
|||||||
export async function add(
|
export async function add(
|
||||||
domainId: string, title: string, content: string, owner: number,
|
domainId: string, title: string, content: string, owner: number,
|
||||||
rule: string, beginAt = new Date(), endAt = new Date(), pids: number[] = [],
|
rule: string, beginAt = new Date(), endAt = new Date(), pids: number[] = [],
|
||||||
rated = false, data: Partial<Tdoc> = {},
|
rated = false, data: Partial<Tdoc<30>> = {},
|
||||||
) {
|
) {
|
||||||
if (!RULES[rule]) throw new ValidationError('rule');
|
if (!RULES[rule]) throw new ValidationError('rule');
|
||||||
if (beginAt >= endAt) throw new ValidationError('beginAt', 'endAt');
|
if (beginAt >= endAt) throw new ValidationError('beginAt', 'endAt');
|
||||||
@@ -682,7 +698,7 @@ export function canShowScoreboard(tdoc: Tdoc<30>, allowPermOverride = true) {
|
|||||||
export async function getScoreboard(
|
export async function getScoreboard(
|
||||||
this: Handler, domainId: string, tid: ObjectID,
|
this: Handler, domainId: string, tid: ObjectID,
|
||||||
isExport = false, ignoreLock = false,
|
isExport = false, ignoreLock = false,
|
||||||
): Promise<[Tdoc<30 | 60>, ScoreboardRow[], Udict, ProblemDict]> {
|
): Promise<[Tdoc<30>, ScoreboardRow[], Udict, ProblemDict]> {
|
||||||
const tdoc = await get(domainId, tid);
|
const tdoc = await get(domainId, tid);
|
||||||
if (!canShowScoreboard.call(this, tdoc)) throw new ContestScoreboardHiddenError(tid);
|
if (!canShowScoreboard.call(this, tdoc)) throw new ContestScoreboardHiddenError(tid);
|
||||||
if (ignoreLock) delete tdoc.lockAt;
|
if (ignoreLock) delete tdoc.lockAt;
|
||||||
@@ -692,6 +708,7 @@ export async function getScoreboard(
|
|||||||
isExport, this.translate.bind(this),
|
isExport, this.translate.bind(this),
|
||||||
tdoc, pdict, tsdocsCursor,
|
tdoc, pdict, tsdocsCursor,
|
||||||
);
|
);
|
||||||
|
await bus.parallel('contest/scoreboard', tdoc, rows, udict, pdict);
|
||||||
return [tdoc, rows, udict, pdict];
|
return [tdoc, rows, udict, pdict];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import type { ProblemSolutionHandler } from '../handler/problem';
|
|||||||
import type { UserRegisterHandler } from '../handler/user';
|
import type { UserRegisterHandler } from '../handler/user';
|
||||||
import type {
|
import type {
|
||||||
DiscussionDoc, DomainDoc, FileInfo,
|
DiscussionDoc, DomainDoc, FileInfo,
|
||||||
MessageDoc, ProblemDoc, RecordDoc,
|
MessageDoc, ProblemDict, ProblemDoc, RecordDoc,
|
||||||
Tdoc, TrainingDoc, User,
|
ScoreboardRow, Tdoc, TrainingDoc, Udict, User,
|
||||||
} from '../interface';
|
} from '../interface';
|
||||||
import type { DocType } from '../model/document';
|
import type { DocType } from '../model/document';
|
||||||
import type { ConnectionHandler, Handler } from './server';
|
import type { ConnectionHandler, Handler } from './server';
|
||||||
@@ -86,8 +86,9 @@ export interface EventMap extends LifecycleEvents, HandlerEvents {
|
|||||||
'problem/addAdditionalFile': (domainId: string, docId: number, name: string, payload: Omit<FileInfo, '_id'>) => VoidReturn
|
'problem/addAdditionalFile': (domainId: string, docId: number, name: string, payload: Omit<FileInfo, '_id'>) => VoidReturn
|
||||||
'problem/delAdditionalFile': (domainId: string, docId: number, name: string[]) => VoidReturn
|
'problem/delAdditionalFile': (domainId: string, docId: number, name: string[]) => VoidReturn
|
||||||
|
|
||||||
'contest/before-add': (payload: Partial<Tdoc>) => VoidReturn
|
'contest/before-add': (payload: Partial<Tdoc<30>>) => VoidReturn
|
||||||
'contest/add': (payload: Partial<Tdoc>, id: ObjectID) => VoidReturn
|
'contest/add': (payload: Partial<Tdoc<30>>, id: ObjectID) => VoidReturn
|
||||||
|
'contest/scoreboard': (tdoc: Tdoc<30>, rows: ScoreboardRow[], udict: Udict, pdict: ProblemDict) => VoidReturn
|
||||||
|
|
||||||
'oplog/log': (type: string, handler: Handler, args: any, data: any) => VoidReturn;
|
'oplog/log': (type: string, handler: Handler, args: any, data: any) => VoidReturn;
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import * as system from './model/system';
|
|||||||
import TaskModel from './model/task';
|
import TaskModel from './model/task';
|
||||||
import user from './model/user';
|
import user from './model/user';
|
||||||
import {
|
import {
|
||||||
|
iterateAllContest,
|
||||||
iterateAllDomain, iterateAllProblem, iterateAllUser,
|
iterateAllDomain, iterateAllProblem, iterateAllUser,
|
||||||
} from './pipelineUtils';
|
} from './pipelineUtils';
|
||||||
import db from './service/db';
|
import db from './service/db';
|
||||||
@@ -570,6 +571,14 @@ const scripts: UpgradeScript[] = [
|
|||||||
await TaskModel.coll.deleteMany({});
|
await TaskModel.coll.deleteMany({});
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
async function _70_71() {
|
||||||
|
await iterateAllContest(async (tdoc) => {
|
||||||
|
if (['acm', 'homework'].includes(tdoc.rule)) {
|
||||||
|
await contest.recalcStatus(tdoc.domainId, tdoc.docId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default scripts;
|
export default scripts;
|
||||||
@@ -142,6 +142,7 @@ env.addGlobal('static_url', (assetName) => {
|
|||||||
env.addGlobal('eval', eval);
|
env.addGlobal('eval', eval);
|
||||||
env.addGlobal('Date', Date);
|
env.addGlobal('Date', Date);
|
||||||
env.addGlobal('Object', Object);
|
env.addGlobal('Object', Object);
|
||||||
|
env.addGlobal('String', String);
|
||||||
env.addGlobal('Math', Math);
|
env.addGlobal('Math', Math);
|
||||||
env.addGlobal('process', process);
|
env.addGlobal('process', process);
|
||||||
env.addGlobal('global', global);
|
env.addGlobal('global', global);
|
||||||
|
|||||||
@@ -40,3 +40,73 @@ for key, value in $contest-color
|
|||||||
&:hover
|
&:hover
|
||||||
background: $primary-color
|
background: $primary-color
|
||||||
border-color: $primary-color
|
border-color: $primary-color
|
||||||
|
|
||||||
|
.contest-timer
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 48px;
|
||||||
|
z-index: 10000;
|
||||||
|
+mobile()
|
||||||
|
visibility: hidden !important;
|
||||||
|
|
||||||
|
.contest-problems
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.375rem;
|
||||||
|
text-align: left;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-variant-ligatures: none!important;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit,minmax(1.5rem,1fr));
|
||||||
|
justify-content: center;
|
||||||
|
row-gap: 0.75rem;
|
||||||
|
column-gap: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
width: rem(180px);
|
||||||
|
|
||||||
|
> a
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
height: 1.5rem;
|
||||||
|
width: 1.5rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
position: relative;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
color: grey;
|
||||||
|
&.pass
|
||||||
|
color: green;
|
||||||
|
&:before
|
||||||
|
background: green;
|
||||||
|
&.fail
|
||||||
|
color: red;
|
||||||
|
&:before
|
||||||
|
background: red;
|
||||||
|
|
||||||
|
&:after
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 2px solid currentColor;
|
||||||
|
border-radius: 6px;
|
||||||
|
opacity: 0.3;
|
||||||
|
|
||||||
|
&.active:after
|
||||||
|
border: 3px solid cyan;
|
||||||
|
|
||||||
|
&:before
|
||||||
|
opacity: 0.5;
|
||||||
|
|
||||||
|
&.pass, &.fail
|
||||||
|
&:not(:hover) .id
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
&:hover .icon
|
||||||
|
display: none;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/ui-default",
|
"name": "@hydrooj/ui-default",
|
||||||
"version": "4.41.17",
|
"version": "4.41.18",
|
||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
|
import { formatSeconds } from '@hydrooj/utils/lib/common';
|
||||||
import NProgress from 'nprogress';
|
import NProgress from 'nprogress';
|
||||||
import { NamedPage } from 'vj/misc/Page';
|
import { NamedPage } from 'vj/misc/Page';
|
||||||
|
import tpl from 'vj/utils/tpl';
|
||||||
|
|
||||||
export default new NamedPage(['contest_detail', 'contest_detail_problem'], () => {
|
const contestTimer = $(tpl`<pre class="contest-timer" style="display:none"></pre>`);
|
||||||
|
contestTimer.appendTo(document.body);
|
||||||
|
|
||||||
|
export default new NamedPage(['contest_detail', 'contest_detail_problem', 'contest_scoreboard'], () => {
|
||||||
const beginAt = new Date(UiContext.tdoc.beginAt).getTime();
|
const beginAt = new Date(UiContext.tdoc.beginAt).getTime();
|
||||||
const endAt = new Date(UiContext.tdoc.endAt).getTime();
|
const endAt = new Date(UiContext.tdoc.endAt).getTime();
|
||||||
NProgress.configure({ trickle: false, showSpinner: false, minimum: 0 });
|
NProgress.configure({ trickle: false, showSpinner: false, minimum: 0 });
|
||||||
function updateProgress() {
|
function updateProgress() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (beginAt <= now && now <= endAt) NProgress.set((now - beginAt) / (endAt - beginAt));
|
if (beginAt <= now && now <= endAt) {
|
||||||
else if (now > endAt) NProgress.set(1);
|
NProgress.set((now - beginAt) / (endAt - beginAt));
|
||||||
else NProgress.set(0);
|
contestTimer.show();
|
||||||
|
contestTimer.text(formatSeconds(Math.floor((endAt - now) / 1000)));
|
||||||
|
} else {
|
||||||
|
if (now > endAt) NProgress.set(1);
|
||||||
|
else NProgress.set(0);
|
||||||
|
contestTimer.hide();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
NProgress.start();
|
NProgress.start();
|
||||||
updateProgress();
|
updateProgress();
|
||||||
setInterval(updateProgress, 5000);
|
setInterval(updateProgress, 1000);
|
||||||
});
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
{% import "components/paginator.html" as paginator with context %}
|
{% import "components/paginator.html" as paginator with context %}
|
||||||
{% extends "layout/basic.html" %}
|
{% extends "layout/basic.html" %}
|
||||||
|
{{ set(UiContext, 'tdoc', tdoc) }}
|
||||||
{% set type = 'homework' if tdoc.rule == 'homework' else 'contest' %}
|
{% set type = 'homework' if tdoc.rule == 'homework' else 'contest' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row"><div class="medium-12 columns scoreboard--{{ tdoc.rule }}">
|
<div class="row"><div class="medium-12 columns scoreboard--{{ tdoc.rule }}">
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="medium-9 columns">
|
<div class="medium-9 columns">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section__header" style="align: center">
|
<div class="section__header">
|
||||||
<h1 class="section__title">
|
<h1 class="section__title">
|
||||||
{% if rdoc %}
|
{% if rdoc %}
|
||||||
<a href="{{ url('record_detail', {rid:rdoc._id}) }}">
|
<a href="{{ url('record_detail', {rid:rdoc._id}) }}">
|
||||||
@@ -46,10 +46,27 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
#{{ pdoc.pid|default(pdoc.docId) }}. {{ pdoc.title }}
|
#{{ pdoc.pid|default(pdoc.docId) }}. {{ pdoc.title }}
|
||||||
</h1>
|
</h1>
|
||||||
|
{% if tdoc %}
|
||||||
|
<div class="section__tools contest-problems">
|
||||||
|
{% for pid in tdoc.pids -%}
|
||||||
|
{% set status = tsdoc.detail[pid].status %}
|
||||||
|
{% set pass = status == STATUS.STATUS_ACCEPTED %}
|
||||||
|
{% set fail = status and not pass %}
|
||||||
|
<a href="{{ url('problem_detail', pid=pid, query={tid:tdoc.docId}) }}"
|
||||||
|
class="{% if pass %}pass{% elif fail %}fail{% endif %}
|
||||||
|
{% if pid==pdoc.docId %} active{% endif %}">
|
||||||
|
<span class="id">{{ String.fromCharCode(65+loop.index0) }}</span>
|
||||||
|
{% if status %}<span class="icon icon-{% if pass %}check{% else %}close{% endif %}"></span>{% endif %}
|
||||||
|
</a>
|
||||||
|
{%- endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="section__body">
|
<div class="section__body">
|
||||||
<ul class="problem__tags">
|
<ul class="problem__tags">
|
||||||
<span class="bp4-tag bp4-large bp4-minimal problem__tag-item">{{ _('ID') }}: {{ pdoc.docId }}</span>
|
{% if not tdoc %}
|
||||||
|
<span class="bp4-tag bp4-large bp4-minimal problem__tag-item">{{ _('ID') }}: {{ pdoc.docId }}</span>
|
||||||
|
{% endif %}
|
||||||
<span class="bp4-tag bp4-large bp4-minimal problem__tag-item icon icon-book">{{ _('problem_type.' + pdoc.config.type).format(pdoc.config.subType) }}</span>
|
<span class="bp4-tag bp4-large bp4-minimal problem__tag-item icon icon-book">{{ _('problem_type.' + pdoc.config.type).format(pdoc.config.subType) }}</span>
|
||||||
{% if pdoc.config.type != 'objective' and pdoc.config.type != 'submit_answer' %}
|
{% if pdoc.config.type != 'objective' and pdoc.config.type != 'submit_answer' %}
|
||||||
<span class="bp4-tag bp4-large bp4-minimal problem__tag-item icon icon-stopwatch">{{ pdoc.config.timeMax if pdoc.config.timeMax == pdoc.config.timeMin else pdoc.config.timeMin+'~'+pdoc.config.timeMax }}ms</span>
|
<span class="bp4-tag bp4-large bp4-minimal problem__tag-item icon icon-stopwatch">{{ pdoc.config.timeMax if pdoc.config.timeMax == pdoc.config.timeMin else pdoc.config.timeMin+'~'+pdoc.config.timeMax }}ms</span>
|
||||||
@@ -59,8 +76,8 @@
|
|||||||
<a class="bp4-tag bp4-large bp4-minimal bp4-interactive problem__tag-item" href="{{ url('record_main', query={pid:pdoc.docId}) }}">{{ _('Tried') }}: {{ pdoc.nSubmit }}</a>
|
<a class="bp4-tag bp4-large bp4-minimal bp4-interactive problem__tag-item" href="{{ url('record_main', query={pid:pdoc.docId}) }}">{{ _('Tried') }}: {{ pdoc.nSubmit }}</a>
|
||||||
<a class="bp4-tag bp4-large bp4-minimal bp4-interactive problem__tag-item" href="{{ url('record_main', query={pid:pdoc.docId, status:1}) }}">{{ _('Accepted') }}: {{ pdoc.nAccept if ((not tdoc) or showAccept) else '?' }}</a>
|
<a class="bp4-tag bp4-large bp4-minimal bp4-interactive problem__tag-item" href="{{ url('record_main', query={pid:pdoc.docId, status:1}) }}">{{ _('Accepted') }}: {{ pdoc.nAccept if ((not tdoc) or showAccept) else '?' }}</a>
|
||||||
<span class="bp4-tag bp4-large bp4-minimal problem__tag-item">{{ _('Difficulty') }}: {{ pdoc['difficulty'] or lib.difficulty(pdoc.nSubmit, pdoc.nAccept) or _('(None)') }}</span>
|
<span class="bp4-tag bp4-large bp4-minimal problem__tag-item">{{ _('Difficulty') }}: {{ pdoc['difficulty'] or lib.difficulty(pdoc.nSubmit, pdoc.nAccept) or _('(None)') }}</span>
|
||||||
|
<span class="bp4-tag bp4-large bp4-minimal bp4-interactive problem__tag-item">{{ _('Uploaded By') }}: {{ user.render_inline(udoc, badge=false) }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="bp4-tag bp4-large bp4-minimal bp4-interactive problem__tag-item">{{ _('Uploaded By') }}: {{ user.render_inline(udoc, badge=false) }}</span>
|
|
||||||
{% if pdoc.tag.length %}
|
{% if pdoc.tag.length %}
|
||||||
<span name="show_tags" class="bp4-tag bp4-large bp4-minimal bp4-interactive problem__tag-item nojs--hide icon icon-tag">{{ _('Tags') + '>' }}</span>
|
<span name="show_tags" class="bp4-tag bp4-large bp4-minimal bp4-interactive problem__tag-item nojs--hide icon icon-tag">{{ _('Tags') + '>' }}</span>
|
||||||
{%- for tag in pdoc.tag -%}
|
{%- for tag in pdoc.tag -%}
|
||||||
|
|||||||
@@ -149,6 +149,20 @@ export function parseMemoryMB(str: string | number, throwOnError = true) {
|
|||||||
return Math.ceil(parseFloat(match[1]) * MEMORY_UNITS[match[2].toLowerCase()]);
|
return Math.ceil(parseFloat(match[1]) * MEMORY_UNITS[match[2].toLowerCase()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _digit2(number: number) {
|
||||||
|
return number < 10 ? `0${number}` : number.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSeconds(_seconds: string | number = '0', showSeconds = true) {
|
||||||
|
const seconds = +_seconds;
|
||||||
|
let res = '{0}:{1}'.format(
|
||||||
|
showSeconds ? _digit2(Math.floor(seconds / 3600)) : Math.floor(seconds / 3600),
|
||||||
|
_digit2(Math.floor((seconds % 3600) / 60)),
|
||||||
|
);
|
||||||
|
if (showSeconds) res += `:${_digit2(seconds % 60)}`;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
export function size(s: number, base = 1) {
|
export function size(s: number, base = 1) {
|
||||||
s *= base;
|
s *= base;
|
||||||
const unit = 1024;
|
const unit = 1024;
|
||||||
|
|||||||
@@ -190,20 +190,6 @@ export function errorMessage(err: Error | string) {
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _digit2(number: number) {
|
|
||||||
return number < 10 ? `0${number}` : number.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatSeconds(_seconds: string | number = '0', showSeconds = true) {
|
|
||||||
const seconds = +_seconds;
|
|
||||||
let res = '{0}:{1}'.format(
|
|
||||||
showSeconds ? _digit2(Math.floor(seconds / 3600)) : Math.floor(seconds / 3600),
|
|
||||||
_digit2(Math.floor((seconds % 3600) / 60)),
|
|
||||||
);
|
|
||||||
if (showSeconds) res += `:${_digit2(seconds % 60)}`;
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function changeErrorType(err: any, Err: any) {
|
export function changeErrorType(err: any, Err: any) {
|
||||||
const e = new Err(err.message);
|
const e = new Err(err.message);
|
||||||
e.stack = err.stack;
|
e.stack = err.stack;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/utils",
|
"name": "@hydrooj/utils",
|
||||||
"version": "1.4.1",
|
"version": "1.4.2",
|
||||||
"description": "hydrooj utils",
|
"description": "hydrooj utils",
|
||||||
"main": "lib/utils.ts",
|
"main": "lib/utils.ts",
|
||||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||||
|
|||||||
Reference in new issue
Block a user