mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
core&ui: add contest balloon list (#578)
Co-authored-by: undefined <i@undefined.moe>
This commit is contained in:
1 parent
8e8c263477
commit
867cd069c5
15 files changed
+341
-8
No files matched your search
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in new issue
Block a user