diff --git a/package.json b/package.json index 9074e04ba..aff4dc8ea 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "latest-version": "7.0.0", "mocha": "^10.1.0", "mongodb": "^3.7.3", - "mongodb-memory-server": "^8.10.0", + "mongodb-memory-server": "^8.10.1", "nyc": "^15.1.0", "ora": "^6.1.2", "semver": "^7.3.8", diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index d1172c3ee..e9e758a6a 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "4.4.2", + "version": "4.4.3", "bin": "bin/hydrooj.js", "main": "src/plugin-api", "module": "src/plugin-api", diff --git a/packages/hydrooj/src/error.ts b/packages/hydrooj/src/error.ts index 9e8e12c31..f52546df7 100644 --- a/packages/hydrooj/src/error.ts +++ b/packages/hydrooj/src/error.ts @@ -137,77 +137,7 @@ export const InvalidOperationError = Err('InvalidOperationError', MethodNotAllow export const NotLaunchedByPM2Error = Err('NotLaunchedByPM2Error', BadRequestError, 'Not launched by PM2.'); export const FileTooLargeError = Err('FileTooLargeError', ValidationError, 'The uploaded file is too long.'); -global.Hydro.error = { - HydroError, - BadRequestError, - BlacklistedError, - ForbiddenError, - NotFoundError, - SendMailError, - LoginError, - CsrfTokenError, - MethodNotAllowedError, - InvalidOperationError, - UserAlreadyExistError, - InvalidTokenError, - UserNotFoundError, - VerifyPasswordError, - ProblemDataNotFoundError, - OpcountExceededError, - PermissionError, - PrivilegeError, - RequireProError, - NoProblemError, - ValidationError, - ProblemNotFoundError, - TrainingNotFoundError, - ContestNotFoundError, - ContestNotEndedError, - RecordNotFoundError, - SolutionNotFoundError, - AlreadyVotedError, - ContestNotAttendedError, - ContestNotLiveError, - ContestScoreboardHiddenError, - ContestAlreadyAttendedError, - DomainAlreadyExistsError, - DomainJoinAlreadyMemberError, - DomainJoinForbiddenError, - InvalidJoinInvitationCodeError, - UserFacingError, - SystemError, - TrainingAlreadyEnrollError, - HomeworkNotLiveError, - HomeworkNotAttendedError, - RemoteOnlineJudgeError, - DiscussionNodeNotFoundError, - DocumentNotFoundError, - DiscussionNotFoundError, - RoleAlreadyExistError, - MessageNotFoundError, - FileTooLargeError, - CurrentPasswordError, - DiscussionLockedError, - FileLimitExceededError, - FileUploadError, - FileExistsError, - HackFailedError, - HackRejudgeFailedError, - CannotDeleteSystemDomainError, - OnlyOwnerCanDeleteDomainError, - NotAssignedError, - BuiltinLoginError, - ProblemAlreadyExistError, - ProblemAlreadyUsedByContestError, - ProblemConfigError, - ProblemIsReferencedError, - ProblemNotAllowPretestError, - ProblemNotAllowLanguageError, - TFAOperationError, - CannotEditSuperAdminError, - NotLaunchedByPM2Error, - AccessDeniedError, -}; +global.Hydro.error = module.exports; /* class FileTypeNotAllowedError(ValidationError): diff --git a/packages/hydrooj/src/handler/record.ts b/packages/hydrooj/src/handler/record.ts index f2e4092e1..f3b8ebfb6 100644 --- a/packages/hydrooj/src/handler/record.ts +++ b/packages/hydrooj/src/handler/record.ts @@ -345,7 +345,7 @@ class RecordDetailConnectionHandler extends ConnectionHandler { if (!problem.canViewBy(pdoc, this.user)) throw new PermissionError(PERM.PERM_VIEW_PROBLEM_HIDDEN); } - this.throttleSend = throttle(this.sendUpdate, 1000); + this.throttleSend = throttle(this.sendUpdate, 1000, { trailing: true }); this.rid = rid.toString(); this.cleanup = bus.on('record/change', this.onRecordChange.bind(this)); this.onRecordChange(rdoc); @@ -368,9 +368,9 @@ class RecordDetailConnectionHandler extends ConnectionHandler { // TODO: frontend doesn't support incremental update // if ($set) this.send({ $set, $push }); if (![STATUS.STATUS_WAITING, STATUS.STATUS_JUDGING, STATUS.STATUS_COMPILING, STATUS.STATUS_FETCHED].includes(rdoc.status)) { - this.sendUpdate(rdoc); this.disconnectTimeout = setTimeout(() => this.close(4001, 'Ended'), 30000); - } else this.throttleSend(rdoc); + } + this.throttleSend(rdoc); } } diff --git a/packages/hydrooj/src/handler/training.ts b/packages/hydrooj/src/handler/training.ts index eda807830..0fdb60ce9 100644 --- a/packages/hydrooj/src/handler/training.ts +++ b/packages/hydrooj/src/handler/training.ts @@ -4,7 +4,6 @@ import { ProblemNotFoundError, ValidationError } from '../error'; import { Tdoc, TrainingDoc } from '../interface'; import paginate from '../lib/paginate'; import { PERM, PRIV, STATUS } from '../model/builtin'; -import DomainModel from '../model/domain'; import problem from '../model/problem'; import * as system from '../model/system'; import * as training from '../model/training'; @@ -108,16 +107,13 @@ class TrainingDetailHandler extends Handler { } } const canViewHidden = this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || this.user._id; - const [udoc, udocs, dudocs, pdict, psdict, selfPsdict] = await Promise.all([ + const [udoc, udict, pdict, psdict, selfPsdict] = await Promise.all([ user.getById(domainId, tdoc.owner), - user.getMulti({ _id: { $in: enrollUsers } }).project({ _id: 1, uname: 1, avatar: 1 }).toArray(), - DomainModel.getDomainUserMulti(domainId, enrollUsers).project({ uid: 1, displayName: 1 }).toArray(), + user.getListForRender(domainId, enrollUsers), problem.getList(domainId, pids, canViewHidden, true), problem.getListStatus(domainId, targetUser, pids), shouldCompare ? problem.getListStatus(domainId, this.user._id, pids) : {}, ]); - const dudict = {}; - for (const dudoc of dudocs) dudict[dudoc.uid] = dudoc.displayName; const donePids = new Set(); const progPids = new Set(); for (const pid in psdict) { @@ -151,7 +147,7 @@ class TrainingDetailHandler extends Handler { done: doneNids.size === tdoc.dag.length, }); this.response.body = { - tdoc, tsdoc, pids, pdict, psdict, ndict, nsdict, udoc, udocs, dudict, selfPsdict, + tdoc, tsdoc, pids, pdict, psdict, ndict, nsdict, udoc, udict, selfPsdict, }; this.response.pjax = 'partials/training_detail.html'; this.response.template = 'training_detail.html'; diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 1ea58d51a..7b038a1c9 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -105,7 +105,15 @@ export interface UserPreferenceDoc { export type ownerInfo = { owner: number, maintainer?: number[] }; export type User = import('./model/user').User; -export type Udict = NumericDictionary; +export type Udict = Record; + +export interface BaseUser { + _id: number; + uname: string; + avatar: string; + displayName: string; +} +export type BaseUserDict = Record; export interface FileInfo { /** storage path */ @@ -507,7 +515,7 @@ export interface ContestRule { scoreboard: ( this: ContestRule, isExport: boolean, _: (s: string) => string, tdoc: Tdoc<30>, pdict: ProblemDict, cursor: Cursor, - ) => Promise<[board: ScoreboardRow[], udict: Udict]>; + ) => Promise<[board: ScoreboardRow[], udict: BaseUserDict]>; ranked: (tdoc: Tdoc<30>, cursor: Cursor) => Promise<[number, ContestStat & T][]>; } diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index a14abc0b9..fc6c15a14 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -178,7 +178,7 @@ const acm = buildContestRule({ async scoreboard(isExport, _, tdoc, pdict, cursor) { const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score && a.time === b.time); const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid); - const udict = await user.getList(tdoc.domainId, uids); + const udict = await user.getListForRender(tdoc.domainId, uids); // Find first accept const first = {}; const data = await document.collStatus.aggregate([ @@ -309,7 +309,7 @@ const oi = buildContestRule({ async scoreboard(isExport, _, tdoc, pdict, cursor) { const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score); const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid); - const udict = await user.getList(tdoc.domainId, uids); + const udict = await user.getListForRender(tdoc.domainId, uids); const psdict = {}; const first = {}; await Promise.all(tdoc.pids.map(async (pid) => { @@ -491,7 +491,7 @@ const homework = buildContestRule({ async scoreboard(isExport, _, tdoc, pdict, cursor) { const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score); const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid); - const udict = await user.getList(tdoc.domainId, uids); + const udict = await user.getListForRender(tdoc.domainId, uids); const columns = await this.scoreboardHeader(isExport, _, tdoc, pdict); const rows: ScoreboardRow[] = [ columns, diff --git a/packages/hydrooj/src/model/user.ts b/packages/hydrooj/src/model/user.ts index 64e4a57dc..b4a9d7e02 100644 --- a/packages/hydrooj/src/model/user.ts +++ b/packages/hydrooj/src/model/user.ts @@ -3,14 +3,14 @@ import LRU from 'lru-cache'; import { Collection, FilterQuery, ObjectID } from 'mongodb'; import { LoginError, UserAlreadyExistError, UserNotFoundError } from '../error'; import { - FileInfo, GDoc, ownerInfo, + BaseUserDict, FileInfo, GDoc, ownerInfo, Udict, Udoc, VUdoc, } from '../interface'; import pwhash from '../lib/hash.hydro'; import * as bus from '../service/bus'; import db from '../service/db'; import { Value } from '../typeutils'; -import { ArgMethod } from '../utils'; +import { ArgMethod, buildProjection } from '../utils'; import { PERM, PRIV } from './builtin'; import domain from './domain'; import * as setting from './setting'; @@ -183,8 +183,10 @@ class UserModel { if (cache.has(`id/${_id}/${domainId}`)) return cache.get(`id/${_id}/${domainId}`) || null; const udoc = await (_id < -999 ? collV : coll).findOne({ _id }); if (!udoc) return null; - const dudoc = await domain.getDomainUser(domainId, udoc); - const groups = await UserModel.listGroup(domainId, _id); + const [dudoc, groups] = await Promise.all([ + domain.getDomainUser(domainId, udoc), + UserModel.listGroup(domainId, _id), + ]); dudoc.group = groups.map((i) => i.name); if (typeof scope === 'string') scope = BigInt(scope); const res = await new User(udoc, dudoc, scope).init(); @@ -339,8 +341,19 @@ class UserModel { return uid; } - static getMulti(params: FilterQuery = {}) { - return coll.find(params); + static getMulti(params: FilterQuery = {}, projection?: (keyof Udoc)[]) { + return projection ? coll.find(params).project(buildProjection(projection)) : coll.find(params); + } + + static async getListForRender(domainId: string, uids: number[]) { + const [udocs, dudocs] = await Promise.all([ + UserModel.getMulti({ _id: { $in: uids } }, ['_id', 'uname', 'avatar']).toArray(), + domain.getDomainUserMulti(domainId, uids).project({ uid: 1, displayName: 1 }).toArray(), + ]); + const udict = {}; + for (const udoc of udocs) udict[udoc._id] = udoc; + for (const dudoc of dudocs) udict[dudoc.uid].displayName = dudoc.displayName; + return udict as BaseUserDict; } @ArgMethod diff --git a/packages/hydrooj/src/service/server.ts b/packages/hydrooj/src/service/server.ts index c5da3a5e1..557ccfef6 100644 --- a/packages/hydrooj/src/service/server.ts +++ b/packages/hydrooj/src/service/server.ts @@ -58,6 +58,9 @@ export interface HydroResponse { type: string; status: number; template?: string; + /** If set, and pjax content was request from client, + * The template will be used for rendering. + */ pjax?: string; redirect?: string; disposition?: string; diff --git a/packages/hydrooj/src/utils.ts b/packages/hydrooj/src/utils.ts index 96655b91c..c427317e8 100644 --- a/packages/hydrooj/src/utils.ts +++ b/packages/hydrooj/src/utils.ts @@ -1,4 +1,4 @@ -export function buildProjection(fields: string[]): Record { +export function buildProjection(fields: Array): Record { const o = {}; for (const k of fields) o[k] = 1; return o; diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 53437ea5f..25b2c210a 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.42.20", + "version": "4.42.21", "author": "undefined ", "license": "AGPL-3.0", "main": "index.ts", diff --git a/packages/ui-default/templates/layout/html5.html b/packages/ui-default/templates/layout/html5.html index e859d98d5..2391f01c5 100644 --- a/packages/ui-default/templates/layout/html5.html +++ b/packages/ui-default/templates/layout/html5.html @@ -32,6 +32,9 @@ --code-font-family: "{{ UserContext.codeFontFamily }}", "monaco", "Source Code Pro", "Consolas", "Lucida Console", "monospace"; --font-ligatures: {{ "normal" if UserContext.codeFontLigatures else "none" }} !important; } + {% if not isIE(handler.request.headers['user-agent']) %} + #panel { display: flex; flex-direction: column; } + {% endif %} {% if process.env.DEV %} diff --git a/packages/ui-default/templates/training_detail.html b/packages/ui-default/templates/training_detail.html index 455cde714..93738b524 100644 --- a/packages/ui-default/templates/training_detail.html +++ b/packages/ui-default/templates/training_detail.html @@ -1,7 +1,7 @@ {% extends "layout/basic.html" %} {% block content %}