mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +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",
|
"latest-version": "7.0.0",
|
||||||
"mocha": "^10.1.0",
|
"mocha": "^10.1.0",
|
||||||
"mongodb": "^3.7.3",
|
"mongodb": "^3.7.3",
|
||||||
"mongodb-memory-server": "^8.10.0",
|
"mongodb-memory-server": "^8.10.1",
|
||||||
"nyc": "^15.1.0",
|
"nyc": "^15.1.0",
|
||||||
"ora": "^6.1.2",
|
"ora": "^6.1.2",
|
||||||
"semver": "^7.3.8",
|
"semver": "^7.3.8",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydrooj",
|
"name": "hydrooj",
|
||||||
"version": "4.4.2",
|
"version": "4.4.3",
|
||||||
"bin": "bin/hydrooj.js",
|
"bin": "bin/hydrooj.js",
|
||||||
"main": "src/plugin-api",
|
"main": "src/plugin-api",
|
||||||
"module": "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 NotLaunchedByPM2Error = Err('NotLaunchedByPM2Error', BadRequestError, 'Not launched by PM2.');
|
||||||
export const FileTooLargeError = Err('FileTooLargeError', ValidationError, 'The uploaded file is too long.');
|
export const FileTooLargeError = Err('FileTooLargeError', ValidationError, 'The uploaded file is too long.');
|
||||||
|
|
||||||
global.Hydro.error = {
|
global.Hydro.error = module.exports;
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
class FileTypeNotAllowedError(ValidationError):
|
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);
|
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.rid = rid.toString();
|
||||||
this.cleanup = bus.on('record/change', this.onRecordChange.bind(this));
|
this.cleanup = bus.on('record/change', this.onRecordChange.bind(this));
|
||||||
this.onRecordChange(rdoc);
|
this.onRecordChange(rdoc);
|
||||||
@@ -368,9 +368,9 @@ class RecordDetailConnectionHandler extends ConnectionHandler {
|
|||||||
// TODO: frontend doesn't support incremental update
|
// TODO: frontend doesn't support incremental update
|
||||||
// if ($set) this.send({ $set, $push });
|
// if ($set) this.send({ $set, $push });
|
||||||
if (![STATUS.STATUS_WAITING, STATUS.STATUS_JUDGING, STATUS.STATUS_COMPILING, STATUS.STATUS_FETCHED].includes(rdoc.status)) {
|
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);
|
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 { Tdoc, TrainingDoc } from '../interface';
|
||||||
import paginate from '../lib/paginate';
|
import paginate from '../lib/paginate';
|
||||||
import { PERM, PRIV, STATUS } from '../model/builtin';
|
import { PERM, PRIV, STATUS } from '../model/builtin';
|
||||||
import DomainModel from '../model/domain';
|
|
||||||
import problem from '../model/problem';
|
import problem from '../model/problem';
|
||||||
import * as system from '../model/system';
|
import * as system from '../model/system';
|
||||||
import * as training from '../model/training';
|
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 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.getById(domainId, tdoc.owner),
|
||||||
user.getMulti({ _id: { $in: enrollUsers } }).project({ _id: 1, uname: 1, avatar: 1 }).toArray(),
|
user.getListForRender(domainId, enrollUsers),
|
||||||
DomainModel.getDomainUserMulti(domainId, enrollUsers).project({ uid: 1, displayName: 1 }).toArray(),
|
|
||||||
problem.getList(domainId, pids, canViewHidden, true),
|
problem.getList(domainId, pids, canViewHidden, true),
|
||||||
problem.getListStatus(domainId, targetUser, pids),
|
problem.getListStatus(domainId, targetUser, pids),
|
||||||
shouldCompare ? problem.getListStatus(domainId, this.user._id, 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 donePids = new Set<number>();
|
||||||
const progPids = new Set<number>();
|
const progPids = new Set<number>();
|
||||||
for (const pid in psdict) {
|
for (const pid in psdict) {
|
||||||
@@ -151,7 +147,7 @@ class TrainingDetailHandler extends Handler {
|
|||||||
done: doneNids.size === tdoc.dag.length,
|
done: doneNids.size === tdoc.dag.length,
|
||||||
});
|
});
|
||||||
this.response.body = {
|
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.pjax = 'partials/training_detail.html';
|
||||||
this.response.template = '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 ownerInfo = { owner: number, maintainer?: number[] };
|
||||||
|
|
||||||
export type User = import('./model/user').User;
|
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 {
|
export interface FileInfo {
|
||||||
/** storage path */
|
/** storage path */
|
||||||
@@ -507,7 +515,7 @@ export interface ContestRule<T = any> {
|
|||||||
scoreboard: (
|
scoreboard: (
|
||||||
this: ContestRule<T>, isExport: boolean, _: (s: string) => string,
|
this: ContestRule<T>, isExport: boolean, _: (s: string) => string,
|
||||||
tdoc: Tdoc<30>, pdict: ProblemDict, cursor: Cursor<ContestStat & T>,
|
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][]>;
|
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) {
|
async scoreboard(isExport, _, tdoc, pdict, cursor) {
|
||||||
const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score && a.time === b.time);
|
const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score && a.time === b.time);
|
||||||
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
|
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
|
// Find first accept
|
||||||
const first = {};
|
const first = {};
|
||||||
const data = await document.collStatus.aggregate([
|
const data = await document.collStatus.aggregate([
|
||||||
@@ -309,7 +309,7 @@ const oi = buildContestRule({
|
|||||||
async scoreboard(isExport, _, tdoc, pdict, cursor) {
|
async scoreboard(isExport, _, tdoc, pdict, cursor) {
|
||||||
const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score);
|
const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score);
|
||||||
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
|
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 psdict = {};
|
||||||
const first = {};
|
const first = {};
|
||||||
await Promise.all(tdoc.pids.map(async (pid) => {
|
await Promise.all(tdoc.pids.map(async (pid) => {
|
||||||
@@ -491,7 +491,7 @@ const homework = buildContestRule({
|
|||||||
async scoreboard(isExport, _, tdoc, pdict, cursor) {
|
async scoreboard(isExport, _, tdoc, pdict, cursor) {
|
||||||
const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score);
|
const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score);
|
||||||
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
|
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 columns = await this.scoreboardHeader(isExport, _, tdoc, pdict);
|
||||||
const rows: ScoreboardRow[] = [
|
const rows: ScoreboardRow[] = [
|
||||||
columns,
|
columns,
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import LRU from 'lru-cache';
|
|||||||
import { Collection, FilterQuery, ObjectID } from 'mongodb';
|
import { Collection, FilterQuery, ObjectID } from 'mongodb';
|
||||||
import { LoginError, UserAlreadyExistError, UserNotFoundError } from '../error';
|
import { LoginError, UserAlreadyExistError, UserNotFoundError } from '../error';
|
||||||
import {
|
import {
|
||||||
FileInfo, GDoc, ownerInfo,
|
BaseUserDict, FileInfo, GDoc, ownerInfo,
|
||||||
Udict, Udoc, VUdoc,
|
Udict, Udoc, VUdoc,
|
||||||
} from '../interface';
|
} from '../interface';
|
||||||
import pwhash from '../lib/hash.hydro';
|
import pwhash from '../lib/hash.hydro';
|
||||||
import * as bus from '../service/bus';
|
import * as bus from '../service/bus';
|
||||||
import db from '../service/db';
|
import db from '../service/db';
|
||||||
import { Value } from '../typeutils';
|
import { Value } from '../typeutils';
|
||||||
import { ArgMethod } from '../utils';
|
import { ArgMethod, buildProjection } from '../utils';
|
||||||
import { PERM, PRIV } from './builtin';
|
import { PERM, PRIV } from './builtin';
|
||||||
import domain from './domain';
|
import domain from './domain';
|
||||||
import * as setting from './setting';
|
import * as setting from './setting';
|
||||||
@@ -183,8 +183,10 @@ class UserModel {
|
|||||||
if (cache.has(`id/${_id}/${domainId}`)) return cache.get(`id/${_id}/${domainId}`) || null;
|
if (cache.has(`id/${_id}/${domainId}`)) return cache.get(`id/${_id}/${domainId}`) || null;
|
||||||
const udoc = await (_id < -999 ? collV : coll).findOne({ _id });
|
const udoc = await (_id < -999 ? collV : coll).findOne({ _id });
|
||||||
if (!udoc) return null;
|
if (!udoc) return null;
|
||||||
const dudoc = await domain.getDomainUser(domainId, udoc);
|
const [dudoc, groups] = await Promise.all([
|
||||||
const groups = await UserModel.listGroup(domainId, _id);
|
domain.getDomainUser(domainId, udoc),
|
||||||
|
UserModel.listGroup(domainId, _id),
|
||||||
|
]);
|
||||||
dudoc.group = groups.map((i) => i.name);
|
dudoc.group = groups.map((i) => i.name);
|
||||||
if (typeof scope === 'string') scope = BigInt(scope);
|
if (typeof scope === 'string') scope = BigInt(scope);
|
||||||
const res = await new User(udoc, dudoc, scope).init();
|
const res = await new User(udoc, dudoc, scope).init();
|
||||||
@@ -339,8 +341,19 @@ class UserModel {
|
|||||||
return uid;
|
return uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getMulti(params: FilterQuery<Udoc> = {}) {
|
static getMulti(params: FilterQuery<Udoc> = {}, projection?: (keyof Udoc)[]) {
|
||||||
return coll.find(params);
|
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
|
@ArgMethod
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ export interface HydroResponse {
|
|||||||
type: string;
|
type: string;
|
||||||
status: number;
|
status: number;
|
||||||
template?: string;
|
template?: string;
|
||||||
|
/** If set, and pjax content was request from client,
|
||||||
|
* The template will be used for rendering.
|
||||||
|
*/
|
||||||
pjax?: string;
|
pjax?: string;
|
||||||
redirect?: string;
|
redirect?: string;
|
||||||
disposition?: 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 = {};
|
const o = {};
|
||||||
for (const k of fields) o[k] = 1;
|
for (const k of fields) o[k] = 1;
|
||||||
return o;
|
return o;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/ui-default",
|
"name": "@hydrooj/ui-default",
|
||||||
"version": "4.42.20",
|
"version": "4.42.21",
|
||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
|
|||||||
@@ -32,6 +32,9 @@
|
|||||||
--code-font-family: "{{ UserContext.codeFontFamily }}", "monaco", "Source Code Pro", "Consolas", "Lucida Console", "monospace";
|
--code-font-family: "{{ UserContext.codeFontFamily }}", "monaco", "Source Code Pro", "Consolas", "Lucida Console", "monospace";
|
||||||
--font-ligatures: {{ "normal" if UserContext.codeFontLigatures else "none" }} !important;
|
--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>
|
</style>
|
||||||
{% if process.env.DEV %}
|
{% if process.env.DEV %}
|
||||||
<link rel="stylesheet" media="all" href="/default.theme.css">
|
<link rel="stylesheet" media="all" href="/default.theme.css">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "layout/basic.html" %}
|
{% extends "layout/basic.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row" data-sticky-parent>
|
<div class="row" data-sticky-parent>
|
||||||
{%- if udocs.length -%}
|
{%- if Object.keys(udict).length -%}
|
||||||
<div class="medium-2 columns mobile--hide">
|
<div class="medium-2 columns mobile--hide">
|
||||||
<div data-sticky="large"><div class="section side">
|
<div data-sticky="large"><div class="section side">
|
||||||
<ol class="menu">
|
<ol class="menu">
|
||||||
@@ -11,14 +11,14 @@
|
|||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
<ul class="enroll_user_menu">
|
<ul class="enroll_user_menu">
|
||||||
{%- for udoc in udocs -%}
|
{%- for uid, udoc in udict -%}
|
||||||
<li class="menu__item enroll_user_menu_item" data-uid="{{ udoc._id }}" data-uname="{{ udoc.uname }}">
|
<li class="menu__item enroll_user_menu_item" data-uid="{{ uid }}" 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 %}">
|
<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">
|
<span class="user-profile-link">
|
||||||
<img class="small user-profile-avatar v-center" loading="lazy" src="{{ avatarUrl(udoc.avatar|default('')) }}" width="20" height="20">
|
<img class="small user-profile-avatar v-center" loading="lazy" src="{{ avatarUrl(udoc.avatar|default('')) }}" width="20" height="20">
|
||||||
<span class="user-profile-name">
|
<span class="user-profile-name">
|
||||||
{% if handler.user.hasPerm(perm.PREM_VIEW_DISPLAYNAME) and dudict[udoc._id] and dudict[udoc._id] != udoc.uname %}
|
{% if handler.user.hasPerm(perm.PREM_VIEW_DISPLAYNAME) and udoc.displayName and udoc.displayName != udoc.uname %}
|
||||||
{{ dudict[udoc._id] }} ({{ udoc.uname }})
|
{{ udoc.displayName }} ({{ udoc.uname }})
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ udoc.uname }}
|
{{ udoc.uname }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in new issue
Block a user