mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
core: optimize scoreboard
This commit is contained in:
1 parent
f08ac664da
commit
af0649f471
13 files changed
+55
-102
No files matched your search
+1
-1
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<number>();
|
||||
const progPids = new Set<number>();
|
||||
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';
|
||||
|
||||
@@ -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<User>;
|
||||
export type Udict = Record<number, User>;
|
||||
|
||||
export interface BaseUser {
|
||||
_id: number;
|
||||
uname: string;
|
||||
avatar: string;
|
||||
displayName: string;
|
||||
}
|
||||
export type BaseUserDict = Record<number, BaseUser>;
|
||||
|
||||
export interface FileInfo {
|
||||
/** storage path */
|
||||
@@ -507,7 +515,7 @@ export interface ContestRule<T = any> {
|
||||
scoreboard: (
|
||||
this: ContestRule<T>, isExport: boolean, _: (s: string) => string,
|
||||
tdoc: Tdoc<30>, pdict: ProblemDict, cursor: Cursor<ContestStat & T>,
|
||||
) => Promise<[board: ScoreboardRow[], udict: Udict]>;
|
||||
) => Promise<[board: ScoreboardRow[], udict: BaseUserDict]>;
|
||||
ranked: (tdoc: Tdoc<30>, cursor: Cursor<ContestStat & T>) => Promise<[number, ContestStat & T][]>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Udoc> = {}) {
|
||||
return coll.find(params);
|
||||
static getMulti(params: FilterQuery<Udoc> = {}, 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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export function buildProjection(fields: string[]): Record<string, 1> {
|
||||
export function buildProjection(fields: Array<string | number>): Record<string, 1> {
|
||||
const o = {};
|
||||
for (const k of fields) o[k] = 1;
|
||||
return o;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/ui-default",
|
||||
"version": "4.42.20",
|
||||
"version": "4.42.21",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "index.ts",
|
||||
|
||||
@@ -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 %}
|
||||
</style>
|
||||
{% if process.env.DEV %}
|
||||
<link rel="stylesheet" media="all" href="/default.theme.css">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "layout/basic.html" %}
|
||||
{% block content %}
|
||||
<div class="row" data-sticky-parent>
|
||||
{%- if udocs.length -%}
|
||||
{%- if Object.keys(udict).length -%}
|
||||
<div class="medium-2 columns mobile--hide">
|
||||
<div data-sticky="large"><div class="section side">
|
||||
<ol class="menu">
|
||||
@@ -11,14 +11,14 @@
|
||||
</form>
|
||||
</li>
|
||||
<ul class="enroll_user_menu">
|
||||
{%- for udoc in udocs -%}
|
||||
<li class="menu__item enroll_user_menu_item" data-uid="{{ udoc._id }}" data-uname="{{ udoc.uname }}">
|
||||
<a href="./{{ tdoc._id }}?uid={{ udoc._id }}" class="menu__link{% if (handler.request.query.uid or handler.user._id) == uid %} active{% endif %}">
|
||||
{%- for uid, udoc in udict -%}
|
||||
<li class="menu__item enroll_user_menu_item" data-uid="{{ uid }}" data-uname="{{ udoc.uname }}">
|
||||
<a href="./{{ tdoc._id }}?uid={{ uid }}" class="menu__link{% if (handler.request.query.uid or handler.user._id) == uid %} active{% endif %}">
|
||||
<span class="user-profile-link">
|
||||
<img class="small user-profile-avatar v-center" loading="lazy" src="{{ avatarUrl(udoc.avatar|default('')) }}" width="20" height="20">
|
||||
<span class="user-profile-name">
|
||||
{% if handler.user.hasPerm(perm.PREM_VIEW_DISPLAYNAME) and dudict[udoc._id] and dudict[udoc._id] != udoc.uname %}
|
||||
{{ dudict[udoc._id] }} ({{ udoc.uname }})
|
||||
{% if handler.user.hasPerm(perm.PREM_VIEW_DISPLAYNAME) and udoc.displayName and udoc.displayName != udoc.uname %}
|
||||
{{ udoc.displayName }} ({{ udoc.uname }})
|
||||
{% else %}
|
||||
{{ udoc.uname }}
|
||||
{% endif %}
|
||||
|
||||
Reference in new issue
Block a user