core: contest: simplify

This commit is contained in:
undefined committed 2022-12-22 23:38:32 +08:00
1 parent 534fc4bb3b
commit 8024122e8f
9 files changed
+32 -62

No files matched your search

+7 -13
View File
@@ -92,10 +92,6 @@ 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),
@@ -122,7 +118,7 @@ export class ContestDetailBaseHandler extends Handler {
name: 'contest_main', name: 'contest_main',
args: {}, args: {},
displayName: 'Back to contest list', displayName: 'Back to contest list',
checker: () => !(this.tdoc.access & contest.AccessControl.LOCK_OTHER), checker: () => true,
}, },
{ {
name: 'contest_detail', name: 'contest_detail',
@@ -161,10 +157,6 @@ export class ContestDetailHandler extends Handler {
contest.getStatus(domainId, tid, this.user._id), 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)) { if (this.tdoc.assign?.length && !this.user.own(this.tdoc)) {
const groups = await user.listGroup(domainId, this.user._id); const groups = await user.listGroup(domainId, this.user._id);
if (!Set.intersection(this.tdoc.assign, groups.map((i) => i.name)).size) { if (!Set.intersection(this.tdoc.assign, groups.map((i) => i.name)).size) {
@@ -193,7 +185,7 @@ export class ContestDetailHandler extends Handler {
name: 'contest_main', name: 'contest_main',
args: {}, args: {},
displayName: 'Back to contest list', displayName: 'Back to contest list',
checker: () => !(this.tdoc.access & contest.AccessControl.LOCK_OTHER), checker: () => true,
}, },
{ {
name: 'contest_detail', name: 'contest_detail',
@@ -225,9 +217,11 @@ export class ContestDetailHandler extends Handler {
async postAttend(domainId: string, tid: ObjectID, code = '') { async postAttend(domainId: string, tid: ObjectID, code = '') {
if (contest.isDone(this.tdoc)) throw new ContestNotLiveError(tid); if (contest.isDone(this.tdoc)) throw new ContestNotLiveError(tid);
if (this.tdoc._code && code !== this.tdoc._code && !this.tdoc.uninvite) throw new InvalidTokenError('Contest Invitation', code); if (this.tdoc._code && code !== this.tdoc._code && !this.tdoc.uninvite) throw new InvalidTokenError('Contest Invitation', code);
await contest.attend(domainId, tid, this.user._id); await contest.attend(
if (this.tdoc.unrankUser.includes(this.user._id) domainId, tid, this.user._id,
|| (this.tdoc._code && code !== this.tdoc._code && this.tdoc.uninvite)) contest.setStatus(domainId, tid, this.user._id, { unrank: true }); (this.tdoc._code && code !== this.tdoc._code && this.tdoc.uninvite)
? { unrank: true } : {},
);
this.back(); this.back();
} }
} }
+2 -2
View File
@@ -258,14 +258,14 @@ export class TrainingFilesHandler extends Handler {
@param('tid', Types.ObjectID) @param('tid', Types.ObjectID)
@post('filename', Types.Name, true) @post('filename', Types.Name, true)
async postUploadFile(domainId: string, tid: ObjectID, filename: string) { async postUploadFile(domainId: string, tid: ObjectID, filename: string) {
if ((this.tdoc.files?.length || 0) >= system.get('limit.training_files')) { if ((this.tdoc.files?.length || 0) >= system.get('limit.contest_files')) {
throw new FileLimitExceededError('count'); throw new FileLimitExceededError('count');
} }
const file = this.request.files?.file; const file = this.request.files?.file;
if (!file) throw new ValidationError('file'); if (!file) throw new ValidationError('file');
const f = statSync(file.filepath); const f = statSync(file.filepath);
const size = Math.sum((this.tdoc.files || []).map((i) => i.size)) + f.size; const size = Math.sum((this.tdoc.files || []).map((i) => i.size)) + f.size;
if (size >= system.get('limit.training_files_size')) { if (size >= system.get('limit.contest_files_size')) {
throw new FileLimitExceededError('size'); throw new FileLimitExceededError('size');
} }
if (!filename) filename = file.originalFilename || String.random(16); if (!filename) filename = file.originalFilename || String.random(16);
-2
View File
@@ -366,9 +366,7 @@ export interface Tdoc<docType = document['TYPE_CONTEST'] | document['TYPE_TRAINI
// For contest // For contest
lockAt?: Date; lockAt?: Date;
unlocked?: boolean; unlocked?: boolean;
access?: number;
autoHide?: boolean; autoHide?: boolean;
unrankUser?: number[];
uninvite?: boolean; uninvite?: boolean;
/** /**
+1 -1
View File
@@ -1,6 +1,6 @@
import { Cursor } from 'mongodb'; 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) => { const ranked: ranked = async (cursor, equ) => {
let last = null; let last = null;
+4 -12
View File
@@ -127,7 +127,7 @@ const acm = buildContestRule({
async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc, meta) { async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc, meta) {
const tsddict = tsdoc.detail || {}; const tsddict = tsdoc.detail || {};
const row: ScoreboardRow = [ const row: ScoreboardRow = [
{ type: 'rank', value: rank ? rank.toString() : '*', raw: rank }, { type: 'rank', value: rank.toString() },
{ type: 'user', value: udoc.uname, raw: tsdoc.uid }, { type: 'user', value: udoc.uname, raw: tsdoc.uid },
]; ];
if (isExport) { if (isExport) {
@@ -263,7 +263,7 @@ const oi = buildContestRule({
async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc, meta) { async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc, meta) {
const tsddict = tsdoc.detail || {}; const tsddict = tsdoc.detail || {};
const row: ScoreboardNode[] = [ const row: ScoreboardNode[] = [
{ type: 'rank', value: rank ? rank.toString() : '*', raw: rank }, { type: 'rank', value: rank.toString() },
{ type: 'user', value: udoc.uname, raw: tsdoc.uid }, { type: 'user', value: udoc.uname, raw: tsdoc.uid },
]; ];
if (isExport) { if (isExport) {
@@ -510,13 +510,6 @@ 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));
} }
@@ -600,9 +593,9 @@ export async function getListStatus(domainId: string, uid: number, tids: ObjectI
return r; 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 { 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) { } catch (e) {
throw new ContestAlreadyAttendedError(tid, uid); throw new ContestAlreadyAttendedError(tid, uid);
} }
@@ -753,7 +746,6 @@ export const statusText = (tdoc: Tdoc, tsdoc?: any) => (
global.Hydro.model.contest = { global.Hydro.model.contest = {
RULES, RULES,
AccessControl,
add, add,
getListStatus, getListStatus,
getMultiStatus, getMultiStatus,
+4 -1
View File
@@ -350,12 +350,15 @@ export async function setStatusIfNotCondition<T extends keyof DocStatusType>(
export async function cappedIncStatus<T extends keyof DocStatusType>( export async function cappedIncStatus<T extends keyof DocStatusType>(
domainId: string, docType: T, docId: DocStatusType[T]['docId'], uid: number, domainId: string, docType: T, docId: DocStatusType[T]['docId'], uid: number,
key: NumberKeys<DocStatusType[T]>, value: number, minValue = -1, maxValue = 1, key: NumberKeys<DocStatusType[T]>, value: number, minValue = -1, maxValue = 1,
setPayload: Partial<DocStatusType[T]> = {},
): Promise<DocStatusType[T]> { ): Promise<DocStatusType[T]> {
assert(value !== 0); assert(value !== 0);
const $not = value > 0 ? { $gte: maxValue } : { $lte: minValue }; 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( const res = await collStatus.findOneAndUpdate(
{ domainId, docType, docId, uid, [key]: { $not } }, { domainId, docType, docId, uid, [key]: { $not } },
{ $inc: { [key]: value } }, operation,
{ upsert: true, returnDocument: 'after' }, { upsert: true, returnDocument: 'after' },
); );
return res.value; return res.value;
+2 -4
View File
@@ -213,10 +213,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.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', 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.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', 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'), 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.training_files', 100, 'number', 'limit.training_files', 'Max files for training'),
Setting('setting_limits', 'limit.training_files_size', 128 * 1024 * 1024, 'number', 'limit.training_files_size', 'Max total file size for training'),
Setting('setting_limits', 'limit.submission', 60, 'number', 'limit.submission', 'Max submission count per minute'), 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.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'), Setting('setting_limits', 'limit.pretest', 60, 'number', 'limit.pretest', 'Max pretest count per minute'),
@@ -3,35 +3,23 @@ import { NamedPage } from 'vj/misc/Page';
const page = new NamedPage('contest_scoreboard', () => { const page = new NamedPage('contest_scoreboard', () => {
const { tdoc } = UiContext; const { tdoc } = UiContext;
let starData = JSON.parse(localStorage.getItem('Hydro.starData')) || {}; const key = `scoreboard-star/${tdoc.domainId}/${tdoc.docId}`;
Object.keys(starData).forEach((key) => { const read = () => JSON.parse(localStorage.getItem(key) || '[]');
if (starData[key].expire < Date.now()) delete starData[key]; const write = (data) => localStorage.setItem(key, JSON.stringify(data));
});
if (!starData[tdoc.docId]) { read().forEach((uid) => $(`.star.user--${uid}`).addClass('activated'));
starData[tdoc.docId] = {
expire: tdoc.endAt + 1000 * 60 * 60 * 24 * 7,
starUids: [],
};
} else {
starData[tdoc.docId].starUids.forEach((uid) => {
console.log(uid);
$(`.star.user--${uid}`).addClass('activated');
});
}
console.log(starData[tdoc.docId].starUids);
localStorage.setItem('Hydro.starData', JSON.stringify(starData));
$('.star').on('click', (e) => { $('.star').on('click', (e) => {
starData = JSON.parse(localStorage.getItem('Hydro.starData')); const star = read();
const $target = $(e.currentTarget); const $target = $(e.currentTarget);
const uid = $target.data('uid'); const uid = $target.data('uid');
if (starData[tdoc.docId].starUids.includes(uid)) { if (star.includes(uid)) {
$target.removeClass('activated'); $target.removeClass('activated');
starData[tdoc.docId].starUids.splice(starData[tdoc.docId].starUids.indexOf(uid), 1); star.splice(star.indexOf(uid), 1);
} else { } else {
$target.addClass('activated'); $target.addClass('activated');
starData[tdoc.docId].starUids.push(uid); star.push(uid);
} }
localStorage.setItem('Hydro.starData', JSON.stringify(starData)); write(star);
}); });
$('.select.filter').on('change', (e) => { $('.select.filter').on('change', (e) => {
@@ -40,9 +28,7 @@ const page = new NamedPage('contest_scoreboard', () => {
$('.data-table tbody tr').show(); $('.data-table tbody tr').show();
} else if (val === 'star') { } else if (val === 'star') {
$('.data-table tbody tr').hide(); $('.data-table tbody tr').hide();
starData[tdoc.docId].starUids.forEach((uid) => { read().forEach((uid) => $(`.star.user--${uid}`).closest('tr').show());
$(`.star.user--${uid}`).closest('tr').show();
});
} else { } else {
$('.data-table tbody tr').hide(); $('.data-table tbody tr').hide();
$('.rank--unrank').closest('tr').show(); $('.rank--unrank').closest('tr').show();
@@ -35,7 +35,6 @@
</select> </select>
</div> </div>
</div> </div>
<h1 style="text-align:center" class="section__title">{{ tdoc.title }}</h1>
{% if model.contest.isLocked(tdoc) %} {% if model.contest.isLocked(tdoc) %}
<div class="section__body no-padding"> <div class="section__body no-padding">
<blockquote class="note"> <blockquote class="note">
@@ -78,7 +77,7 @@
{%- for column in row -%} {%- for column in row -%}
<td class="col--{{ rows[0][loop.index0]['type'] }}"{% if column.style %} style="{{ column.style }}"{% endif %}> <td class="col--{{ rows[0][loop.index0]['type'] }}"{% if column.style %} style="{{ column.style }}"{% endif %}>
{%- if column.type == 'rank' -%} {%- if column.type == 'rank' -%}
<span class="{{ 'rank--normal' if column.raw else 'rank--unrank' }}">{{ column.value|string|nl2br|safe }}</span> <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' -%} {%- elif column.type == 'user' -%}
{%- set canView = canViewAll or handler.user._id == column.raw -%} {%- set canView = canViewAll or handler.user._id == column.raw -%}
<button class="star user--{{ column.raw }}" data-uid="{{ column.raw }}"> <button class="star user--{{ column.raw }}" data-uid="{{ column.raw }}">