core&ui: add contest balloon list (#578)

Co-authored-by: undefined <i@undefined.moe>
This commit is contained in:
pandaandundefined authored and GitHub committed 2023-11-05 02:51:25 +08:00
1 parent 8e8c263477
commit 867cd069c5
15 files changed
+341 -8

No files matched your search

+52 -2
View File
@@ -4,8 +4,9 @@ import { pick } from 'lodash';
import moment from 'moment-timezone';
import { ObjectId } from 'mongodb';
import {
Counter, sortFiles, streamToBuffer, Time,
Counter, sortFiles, streamToBuffer, Time, yaml,
} from '@hydrooj/utils/lib/utils';
import { Context } from '../context';
import {
BadRequestError, ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError,
ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError,
@@ -663,7 +664,55 @@ export class ContestUserHandler extends ContestManagementBaseHandler {
this.back();
}
}
export async function apply(ctx) {
export class ContestBalloonHandler extends ContestManagementBaseHandler {
@param('tid', Types.ObjectId)
@param('todo', Types.Boolean)
async get(domainId: string, tid: ObjectId, todo = false) {
const bdocs = await contest.getMultiBalloon(domainId, tid, {
...todo ? { sent: { $exists: false } } : {},
...(!this.tdoc.lockAt || this.user.hasPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD))
? {} : { _id: { $lt: this.tdoc.lockAt } },
}).sort({ _id: -1 }).project({ uid: 1 }).toArray();
const uids = bdocs.map((i) => i.uid).concat(bdocs.filter((i) => i.sent).map((i) => i.sent));
this.response.body = {
tdoc: this.tdoc,
tsdoc: this.tsdoc,
owner_udoc: await user.getById(domainId, this.tdoc.owner),
pdict: await problem.getList(domainId, this.tdoc.pids, true, true, problem.PROJECTION_CONTEST_LIST),
bdocs,
udict: await user.getListForRender(domainId, uids),
};
this.response.pjax = 'partials/contest_balloon.html';
this.response.template = 'contest_balloon.html';
}
@param('tid', Types.ObjectId)
@param('color', Types.Content)
async postSetColor(domainId: string, tid: ObjectId, color: string) {
const config = yaml.load(color);
if (typeof config !== 'object') throw new ValidationError('color');
const balloon = {};
for (const pid of this.tdoc.pids) {
if (!config[pid]) throw new ValidationError('color');
balloon[pid] = config[pid.toString()];
}
await contest.edit(domainId, tid, { balloon });
this.back();
}
@param('tid', Types.ObjectId)
@param('balloon', Types.ObjectId)
async postDone(domainId: string, tid: ObjectId, bid: ObjectId) {
const balloon = await contest.getBalloon(domainId, tid, bid);
if (!balloon) throw new ValidationError('balloon');
if (balloon.sent) throw new ValidationError('balloon', null, 'Balloon already sent');
await contest.updateBalloon(domainId, tid, bid, { sent: this.user._id, sentAt: new Date() });
this.back();
}
}
export async function apply(ctx: Context) {
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);
@@ -675,4 +724,5 @@ export async function apply(ctx) {
ctx.Route('contest_code', '/contest/:tid/code', ContestCodeHandler, 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);
ctx.Route('contest_balloon', '/contest/:tid/balloon', ContestBalloonHandler, PERM.PERM_VIEW_CONTEST);
}
+14
View File
@@ -393,6 +393,7 @@ export interface Tdoc<docType = document['TYPE_CONTEST'] | document['TYPE_TRAINI
lockAt?: Date;
unlocked?: boolean;
autoHide?: boolean;
balloon?: Record<number, string>;
/**
* In hours
@@ -651,6 +652,18 @@ export interface DiscussionHistoryDoc {
ip: string;
}
export interface ContestBalloonDoc {
_id: ObjectId;
domainId: string;
tid: ObjectId;
pid: number;
uid: number;
first?: boolean;
/** Sent by */
sent?: number;
sentAt?: Date;
}
declare module './service/db' {
interface Collections {
'blacklist': BlacklistDoc;
@@ -678,6 +691,7 @@ declare module './service/db' {
'event': EventDoc;
'opcount': OpCountDoc;
'schedule': Schedule;
'contest.balloon': ContestBalloonDoc;
}
}
+36 -3
View File
@@ -11,6 +11,7 @@ import {
} from '../interface';
import ranked from '../lib/rank';
import * as bus from '../service/bus';
import db from '../service/db';
import type { Handler } from '../service/server';
import { PERM, STATUS, STATUS_SHORT_TEXTS } from './builtin';
import * as document from './document';
@@ -655,6 +656,8 @@ export const RULES: ContestRules = {
acm, oi, homework, ioi, ledo, strictioi,
};
const collBalloon = db.collection('contest.balloon');
function _getStatusJournal(tsdoc) {
return tsdoc.journal.sort((a, b) => (a.rid.getTimestamp() - b.rid.getTimestamp()));
}
@@ -705,6 +708,32 @@ export async function getRelated(domainId: string, pid: number, rule?: string) {
return await document.getMulti(domainId, document.TYPE_CONTEST, { pids: pid, rule: rule || { $in: rules } }).toArray();
}
export async function addBalloon(domainId: string, tid: ObjectId, uid: number, rid: ObjectId, pid: number) {
const balloon = await collBalloon.findOne({
domainId, tid, pid, uid,
});
if (balloon) return null;
const bdcount = await collBalloon.countDocuments({ domainId, tid, pid });
const newBdoc = {
_id: rid, domainId, tid, pid, uid, ...(!bdcount ? { first: true } : {}),
};
await collBalloon.insertOne(newBdoc);
bus.broadcast('contest/balloon', domainId, tid, newBdoc);
return rid;
}
export async function getBalloon(domainId: string, tid: ObjectId, _id: ObjectId) {
return await collBalloon.findOne({ domainId, tid, _id });
}
export function getMultiBalloon(domainId: string, tid: ObjectId, query: any = {}) {
return collBalloon.find({ domainId, tid, ...query });
}
export async function updateBalloon(domainId: string, tid: ObjectId, _id: ObjectId, $set: any) {
return await collBalloon.findOneAndUpdate({ domainId, tid, _id }, { $set });
}
export async function getStatus(domainId: string, tid: ObjectId, uid: number) {
return await document.getStatus(domainId, document.TYPE_CONTEST, tid, uid);
}
@@ -726,6 +755,7 @@ export async function updateStatus(
status = STATUS.STATUS_WRONG_ANSWER, score = 0, subtasks: Record<number, SubtaskResult> = {},
) {
const tdoc = await get(domainId, tid);
if (tdoc.balloon && status === STATUS.STATUS_ACCEPTED) await addBalloon(domainId, tid, uid, rid, pid);
return await _updateStatus(tdoc, uid, rid, pid, status, score, subtasks);
}
@@ -778,10 +808,9 @@ export function isDone(tdoc: Tdoc, tsdoc?: any) {
return false;
}
export function isLocked(tdoc: Tdoc) {
export function isLocked(tdoc: Tdoc, time = new Date()) {
if (!tdoc.lockAt) return false;
const now = new Date();
return tdoc.lockAt < now && !tdoc.unlocked;
return tdoc.lockAt < time && !tdoc.unlocked;
}
export function isExtended(tdoc: Tdoc) {
@@ -910,6 +939,10 @@ global.Hydro.model.contest = {
getAndListStatus,
recalcStatus,
unlockScoreboard,
getBalloon,
addBalloon,
getMultiBalloon,
updateBalloon,
canShowRecord,
canShowSelfRecord,
canShowScoreboard,
+2 -1
View File
@@ -6,7 +6,7 @@ import pm2 from '@hydrooj/utils/lib/locate-pm2';
import type { ProblemSolutionHandler } from '../handler/problem';
import type { UserRegisterHandler } from '../handler/user';
import type {
BaseUserDict, DiscussionDoc, DomainDoc, FileInfo,
BaseUserDict, ContestBalloonDoc, DiscussionDoc, DomainDoc, FileInfo,
MessageDoc, ProblemDict, ProblemDoc, RecordDoc,
ScoreboardRow, Tdoc, TrainingDoc, User,
} from '../interface';
@@ -92,6 +92,7 @@ export interface EventMap extends LifecycleEvents, HandlerEvents {
'contest/before-add': (payload: Partial<Tdoc<30>>) => VoidReturn
'contest/add': (payload: Partial<Tdoc<30>>, id: ObjectId) => VoidReturn
'contest/scoreboard': (tdoc: Tdoc<30>, rows: ScoreboardRow[], udict: BaseUserDict, pdict: ProblemDict) => VoidReturn
'contest/balloon': (domainId: string, tid: ObjectId, bdoc: ContestBalloonDoc) => VoidReturn
'oplog/log': (type: string, handler: Handler, args: any, data: any) => VoidReturn;