core: feat: problem group-assign

This commit is contained in:
undefined committed 2022-01-07 21:37:19 +08:00
1 parent f699d67019
commit 8fd23e6cea
21 files changed
+250 -104

No files matched your search

+1
View File
@@ -439,6 +439,7 @@ Location: 位置
Login to Attend Contest: 登录后参加比赛 Login to Attend Contest: 登录后参加比赛
Login to Claim Homework: 登录后认领作业 Login to Claim Homework: 登录后认领作业
Login to Create a Discussion: 登录后创建讨论 Login to Create a Discussion: 登录后创建讨论
Login to join training plan: 登录以参加训练计划
Login to Submit: 登录后递交 Login to Submit: 登录后递交
Login: 登录 Login: 登录
Logout All Sessions: 注销所有会话 Logout All Sessions: 注销所有会话
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "hydrooj", "name": "hydrooj",
"version": "3.2.43", "version": "3.3.0",
"bin": "bin/hydrooj.js", "bin": "bin/hydrooj.js",
"main": "src/loader", "main": "src/loader",
"module": "src/loader", "module": "src/loader",
+19 -5
View File
@@ -3,7 +3,7 @@ import moment from 'moment-timezone';
import { ObjectID } from 'mongodb'; import { ObjectID } from 'mongodb';
import { Time } from '@hydrooj/utils/lib/utils'; import { Time } from '@hydrooj/utils/lib/utils';
import { import {
ContestNotFoundError, ContestNotLiveError, InvalidTokenError, ContestNotFoundError, ContestNotLiveError, ForbiddenError, InvalidTokenError,
PermissionError, ValidationError, PermissionError, ValidationError,
} from '../error'; } from '../error';
import { Tdoc } from '../interface'; import { Tdoc } from '../interface';
@@ -74,6 +74,18 @@ export class ContestListHandler extends Handler {
} }
export class ContestDetailHandler extends Handler { export class ContestDetailHandler extends Handler {
@param('tid', Types.ObjectID)
@param('page', Types.PositiveInt, true)
async prepare(domainId: string, tid: ObjectID) {
const tdoc = await contest.get(domainId, tid);
if (tdoc.assign) {
const groups = await user.listGroup(domainId, this.user._id);
if (!Set.intersection(tdoc.assign, groups.map((i) => i.name)).size) {
throw new ForbiddenError('You are not assigned.');
}
}
}
@param('tid', Types.ObjectID) @param('tid', Types.ObjectID)
@param('page', Types.PositiveInt, true) @param('page', Types.PositiveInt, true)
async get(domainId: string, tid: ObjectID, page = 1) { async get(domainId: string, tid: ObjectID, page = 1) {
@@ -238,9 +250,11 @@ export class ContestEditHandler extends Handler {
@param('rated', Types.Boolean) @param('rated', Types.Boolean)
@param('code', Types.String, true) @param('code', Types.String, true)
@param('autoHide', Types.String, true) @param('autoHide', Types.String, true)
@param('assign', Types.Array, true)
async post( async post(
domainId: string, tid: ObjectID, beginAtDate: string, beginAtTime: string, duration: number, domainId: string, tid: ObjectID, beginAtDate: string, beginAtTime: string, duration: number,
title: string, content: string, rule: string, _pids: string, rated = false, _code = '', autoHide = false, title: string, content: string, rule: string, _pids: string, rated = false,
_code = '', autoHide = false, assign: string[] = null,
) { ) {
if (autoHide) this.checkPerm(PERM.PERM_EDIT_PROBLEM); if (autoHide) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
const pids = _pids.replace(/,/g, ',').split(',').map((i) => +i).filter((i) => i); const pids = _pids.replace(/,/g, ',').split(',').map((i) => +i).filter((i) => i);
@@ -249,7 +263,7 @@ export class ContestEditHandler extends Handler {
const endAt = beginAtMoment.clone().add(duration, 'hours').toDate(); const endAt = beginAtMoment.clone().add(duration, 'hours').toDate();
if (beginAtMoment.isSameOrAfter(endAt)) throw new ValidationError('duration'); if (beginAtMoment.isSameOrAfter(endAt)) throw new ValidationError('duration');
const beginAt = beginAtMoment.toDate(); const beginAt = beginAtMoment.toDate();
await problem.getList(domainId, pids, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || this.user._id, true); await problem.getList(domainId, pids, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || this.user._id, this.user.group, true);
if (tid) { if (tid) {
await contest.edit(domainId, tid, { await contest.edit(domainId, tid, {
title, content, rule, beginAt, endAt, pids, rated, title, content, rule, beginAt, endAt, pids, rated,
@@ -267,13 +281,13 @@ export class ContestEditHandler extends Handler {
await TaskModel.deleteMany(task); await TaskModel.deleteMany(task);
if (Date.now() <= endAt.getTime() && autoHide) { if (Date.now() <= endAt.getTime() && autoHide) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
for (const pid of pids) await problem.edit(domainId, pid, { hidden: true }); await Promise.all(pids.map((pid) => problem.edit(domainId, pid, { hidden: true })));
await TaskModel.add({ await TaskModel.add({
...task, ...task,
executeAfter: endAt, executeAfter: endAt,
}); });
} }
await contest.edit(domainId, tid, { _code, autoHide }); await contest.edit(domainId, tid, { assign, _code, autoHide });
this.response.body = { tid }; this.response.body = { tid };
this.response.redirect = this.url('contest_detail', { tid }); this.response.redirect = this.url('contest_detail', { tid });
} }
+6 -4
View File
@@ -58,7 +58,11 @@ class DiscussionHandler extends Handler {
// TODO(twd2): exclude problem/contest discussions? // TODO(twd2): exclude problem/contest discussions?
// TODO(iceboy): continuation based pagination. // TODO(iceboy): continuation based pagination.
this.vnode = await discussion.getVnode(domainId, typeMapper[type], name, this.user._id); this.vnode = await discussion.getVnode(domainId, typeMapper[type], name, this.user._id);
if (this.vnode.hidden) this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN); if (this.vnode.assign?.length) {
if (!Set.intersection(this.vnode.assign, this.user.group).size) {
throw new PermissionError(PERM.PERM_VIEW_PROBLEM_HIDDEN);
}
}
if (this.ddoc) { if (this.ddoc) {
this.ddoc.parentType = this.ddoc.parentType || this.vnode.type; this.ddoc.parentType = this.ddoc.parentType || this.vnode.type;
this.ddoc.parentId = this.ddoc.parentId || this.vnode.id; this.ddoc.parentId = this.ddoc.parentId || this.vnode.id;
@@ -78,9 +82,7 @@ class DiscussionMainHandler extends Handler {
); );
const udict = await user.getList(domainId, ddocs.map((ddoc) => ddoc.owner)); const udict = await user.getList(domainId, ddocs.map((ddoc) => ddoc.owner));
const [vndict, vnodes] = await Promise.all([ const [vndict, vnodes] = await Promise.all([
discussion.getListVnodes( discussion.getListVnodes(domainId, ddocs, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN), this.user.group),
domainId, ddocs, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN),
),
discussion.getNodes(domainId), discussion.getNodes(domainId),
]); ]);
this.response.template = 'discussion_main_or_node.html'; this.response.template = 'discussion_main_or_node.html';
+56 -2
View File
@@ -3,7 +3,7 @@ import { Dictionary } from 'lodash';
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import { import {
DomainJoinAlreadyMemberError, DomainJoinForbiddenError, ForbiddenError, DomainJoinAlreadyMemberError, DomainJoinForbiddenError, ForbiddenError,
InvalidJoinInvitationCodeError, RoleAlreadyExistError, ValidationError, InvalidJoinInvitationCodeError, PermissionError, RoleAlreadyExistError, ValidationError,
} from '../error'; } from '../error';
import type { DomainDoc } from '../interface'; import type { DomainDoc } from '../interface';
import avatar from '../lib/avatar'; import avatar from '../lib/avatar';
@@ -19,6 +19,40 @@ import {
Handler, param, post, query, Route, Types, Handler, param, post, query, Route, Types,
} from '../service/server'; } from '../service/server';
import { log2 } from '../utils'; import { log2 } from '../utils';
import { registerResolver, registerValue } from './api';
registerValue('GroupInfo', [
['name', 'String!'],
['uids', '[Int]!'],
]);
registerResolver('Query', 'domain(id: String)', 'Domain', async (args, ctx) => {
const ddoc = await domain.get(args.id);
if (!ddoc) return null;
const udoc = await user.getById(args.id, ctx.user._id);
if (!udoc.hasPerm(PERM.PERM_VIEW) && !udoc.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN)) return null;
ctx.udoc = udoc;
return ddoc;
});
registerResolver('Domain', 'manage', 'DomainManage', async (args, ctx) => {
if (!ctx.udoc.hasPerm(PERM.PERM_EDIT_DOMAIN)) throw new PermissionError(PERM.PERM_EDIT_DOMAIN);
return {};
});
registerResolver('DomainManage', 'group', 'DomainGroup', (args, ctx) => ctx.parent);
registerResolver(
'DomainGroup', 'list(uid: Int)', '[GroupInfo]',
(args, ctx) => user.listGroup(ctx.parent._id, args.uid),
);
registerResolver(
'DomainGroup', 'update(name: String!, uids: [Int]!)', 'Boolean',
async (args, ctx) => !!(await user.updateGroup(ctx.parent._id, args.name, args.uids)).upsertedCount,
);
registerResolver(
'DomainGroup', 'del(name: String!)', 'Boolean',
async (args, ctx) => !!(await user.delGroup(ctx.parent._id, args.name)).deletedCount,
);
class DomainRankHandler extends Handler { class DomainRankHandler extends Handler {
@query('page', Types.PositiveInt, true) @query('page', Types.PositiveInt, true)
@@ -184,7 +218,7 @@ class DomainRoleHandler extends ManageHandler {
@param('roles', Types.Array) @param('roles', Types.Array)
async postDelete(domainId: string, roles: string[]) { async postDelete(domainId: string, roles: string[]) {
if (Set.intersection(new Set(roles), new Set(['root', 'default', 'guest'])).size > 0) { if (Set.intersection(roles, ['root', 'default', 'guest']).size > 0) {
throw new ValidationError('role', null, 'You cannot delete root, default or guest roles'); throw new ValidationError('role', null, 'You cannot delete root, default or guest roles');
} }
await domain.deleteRoles(domainId, roles); await domain.deleteRoles(domainId, roles);
@@ -231,6 +265,25 @@ class DomainJoinApplicationsHandler extends ManageHandler {
} }
} }
class DomainUserGroupHandler extends ManageHandler {
async get() {
this.response.body = 'domain_group.html';
}
@param('name', Types.Name)
async postDel(domainId: string, name: string) {
await user.delGroup(domainId, name);
this.back();
}
@param('name', Types.Name)
@param('uids', Types.NumericArray)
async postUpdate(domainId: string, name: string, uids: number[]) {
await user.updateGroup(domainId, name, uids);
this.back();
}
}
class DomainJoinHandler extends Handler { class DomainJoinHandler extends Handler {
joinSettings: any; joinSettings: any;
noCheckPermView = true; noCheckPermView = true;
@@ -283,6 +336,7 @@ export async function apply() {
Route('domain_user', '/domain/user', DomainUserHandler); Route('domain_user', '/domain/user', DomainUserHandler);
Route('domain_permission', '/domain/permission', DomainPermissionHandler); Route('domain_permission', '/domain/permission', DomainPermissionHandler);
Route('domain_role', '/domain/role', DomainRoleHandler); Route('domain_role', '/domain/role', DomainRoleHandler);
Route('domain_group', '/domain/group', DomainUserGroupHandler);
Route('domain_join_applications', '/domain/join_applications', DomainJoinApplicationsHandler); Route('domain_join_applications', '/domain/join_applications', DomainJoinApplicationsHandler);
Route('domain_join', '/domain/join', DomainJoinHandler, PRIV.PRIV_USER_PROFILE); Route('domain_join', '/domain/join', DomainJoinHandler, PRIV.PRIV_USER_PROFILE);
Route('domain_search', '/domain/search', DomainSearchHandler, PRIV.PRIV_USER_PROFILE); Route('domain_search', '/domain/search', DomainSearchHandler, PRIV.PRIV_USER_PROFILE);
+2 -4
View File
@@ -75,9 +75,7 @@ class HomeHandler extends Handler {
async getDiscussion(domainId: string, limit = 20) { async getDiscussion(domainId: string, limit = 20) {
if (this.user.hasPerm(PERM.PERM_VIEW_DISCUSSION)) { if (this.user.hasPerm(PERM.PERM_VIEW_DISCUSSION)) {
const ddocs = await discussion.getMulti(domainId).limit(limit).toArray(); const ddocs = await discussion.getMulti(domainId).limit(limit).toArray();
const vndict = await discussion.getListVnodes( const vndict = await discussion.getListVnodes(domainId, ddocs, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN), this.user.group);
domainId, ddocs, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN),
);
this.collectUser(ddocs.map((ddoc) => ddoc.owner)); this.collectUser(ddocs.map((ddoc) => ddoc.owner));
return ['discussion', ddocs, vndict]; return ['discussion', ddocs, vndict];
} }
@@ -103,7 +101,7 @@ class HomeHandler extends Handler {
for (const psdoc of psdocs) psdict[psdoc.docId] = psdoc; for (const psdoc of psdocs) psdict[psdoc.docId] = psdoc;
const pdict = await ProblemModel.getList( const pdict = await ProblemModel.getList(
domainId, psdocs.map((pdoc) => pdoc.docId), domainId, psdocs.map((pdoc) => pdoc.docId),
this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || this.user._id, false, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || this.user._id, this.user.group, false,
); );
const pdocs = Object.keys(pdict).filter((i) => +i).map((i) => pdict[i]); const pdocs = Object.keys(pdict).filter((i) => +i).map((i) => pdict[i]);
return ['problem', pdocs, psdict]; return ['problem', pdocs, psdict];
+14 -2
View File
@@ -4,7 +4,8 @@ import moment from 'moment-timezone';
import { ObjectID } from 'mongodb'; import { ObjectID } from 'mongodb';
import { Time } from '@hydrooj/utils/lib/utils'; import { Time } from '@hydrooj/utils/lib/utils';
import { import {
ContestNotFoundError, HomeworkNotLiveError, ValidationError, ContestNotFoundError, ForbiddenError, HomeworkNotLiveError,
ValidationError,
} from '../error'; } from '../error';
import { PenaltyRules } from '../interface'; import { PenaltyRules } from '../interface';
import paginate from '../lib/paginate'; import paginate from '../lib/paginate';
@@ -40,6 +41,17 @@ class HomeworkMainHandler extends Handler {
} }
class HomeworkDetailHandler extends Handler { class HomeworkDetailHandler extends Handler {
@param('tid', Types.ObjectID)
async prepare(domainId: string, tid: ObjectID) {
const tdoc = await contest.get(domainId, tid);
if (tdoc.rule !== 'homework') throw new ContestNotFoundError(domainId, tid);
if (tdoc.assign) {
if (!Set.intersection(tdoc.assign, this.user.group).size) {
throw new ForbiddenError('You are not assigned.');
}
}
}
@param('tid', Types.ObjectID) @param('tid', Types.ObjectID)
@param('page', Types.PositiveInt, true) @param('page', Types.PositiveInt, true)
async get(domainId: string, tid: ObjectID, page = 1) { async get(domainId: string, tid: ObjectID, page = 1) {
@@ -156,7 +168,7 @@ class HomeworkEditHandler extends Handler {
const endAt = penaltySince.clone().add(extensionDays, 'days'); const endAt = penaltySince.clone().add(extensionDays, 'days');
if (beginAt.isSameOrAfter(penaltySince)) throw new ValidationError('endAtDate', 'endAtTime'); if (beginAt.isSameOrAfter(penaltySince)) throw new ValidationError('endAtDate', 'endAtTime');
if (penaltySince.isAfter(endAt)) throw new ValidationError('extensionDays'); if (penaltySince.isAfter(endAt)) throw new ValidationError('extensionDays');
await problem.getList(domainId, pids, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || this.user._id, true); await problem.getList(domainId, pids, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || this.user._id, this.user.group, true);
if (!tid) { if (!tid) {
tid = await contest.add(domainId, title, content, this.user._id, tid = await contest.add(domainId, title, content, this.user._id,
'homework', beginAt.toDate(), endAt.toDate(), pids, rated, 'homework', beginAt.toDate(), endAt.toDate(), pids, rated,
+52 -24
View File
@@ -1,17 +1,18 @@
import AdmZip from 'adm-zip'; import AdmZip from 'adm-zip';
import { statSync } from 'fs-extra'; import { statSync } from 'fs-extra';
import { flatten, intersection, isSafeInteger } from 'lodash'; import { intersection, isSafeInteger } from 'lodash';
import { FilterQuery, ObjectID } from 'mongodb'; import { FilterQuery, ObjectID } from 'mongodb';
import { sortFiles } from '@hydrooj/utils/lib/utils'; import { sortFiles } from '@hydrooj/utils/lib/utils';
import { import {
BadRequestError, ContestNotAttendedError, ContestNotEndedError, BadRequestError, ContestNotAttendedError, ContestNotEndedError,
ContestNotFoundError, ContestNotLiveError, ContestNotFoundError, ContestNotLiveError,
ForbiddenError, NoProblemError, NotFoundError, ForbiddenError, NoProblemError, NotFoundError,
PermissionError, ProblemNotFoundError, SolutionNotFoundError, ValidationError, PermissionError, ProblemNotFoundError, SolutionNotFoundError,
ValidationError,
} from '../error'; } from '../error';
import { import {
ProblemConfig, ProblemConfig, ProblemDoc, ProblemStatusDoc,
ProblemDoc, ProblemStatusDoc, Tdoc, User, Tdoc, User,
} from '../interface'; } from '../interface';
import paginate from '../lib/paginate'; import paginate from '../lib/paginate';
import { isPid, parsePid as convertPid } from '../lib/validator'; import { isPid, parsePid as convertPid } from '../lib/validator';
@@ -32,7 +33,7 @@ import {
} from '../service/server'; } from '../service/server';
import { registerResolver, registerValue } from './api'; import { registerResolver, registerValue } from './api';
export const parseCategory = (value: string) => flatten(value.replace(/,/g, ',').split(',')).map((e) => e.trim()); export const parseCategory = (value: string) => value.replace(/,/g, ',').split(',').map((e) => e.trim());
export const parsePid = (value: string) => (isSafeInteger(value) ? +value : value); export const parsePid = (value: string) => (isSafeInteger(value) ? +value : value);
registerValue('FileInfo', [ registerValue('FileInfo', [
@@ -58,9 +59,7 @@ registerValue('Problem', [
['difficulty', 'Int'], ['difficulty', 'Int'],
['tag', '[String]'], ['tag', '[String]'],
['hidden', 'Boolean'], ['hidden', 'Boolean'],
]); ['assign', '[String]'],
registerValue('ProblemManage', [
['delete', 'Boolean!'],
]); ]);
registerResolver( registerResolver(
@@ -68,7 +67,12 @@ registerResolver(
async (arg, ctx) => { async (arg, ctx) => {
const pdoc = await problem.get(ctx.domainId, arg.pid || arg.id); const pdoc = await problem.get(ctx.domainId, arg.pid || arg.id);
if (!pdoc) return null; if (!pdoc) return null;
if (pdoc.hidden && !ctx.user.own(pdoc)) ctx.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN); if (pdoc.hidden) ctx.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
if (pdoc.assign.length && !ctx.user.own(pdoc)) {
if (!Set.intersection(new Set(pdoc.assign), new Set(ctx.user.group)).size) {
throw new PermissionError(PERM.PERM_VIEW_PROBLEM_HIDDEN);
}
}
ctx.pdoc = pdoc; ctx.pdoc = pdoc;
return pdoc; return pdoc;
}, },
@@ -89,10 +93,33 @@ registerResolver(
}, },
); );
registerResolver( registerResolver(
'ProblemManage', 'edit(title: String, content: String, tag: [String], hidden: Boolean)', 'Problem!', 'ProblemManage', 'edit(title: String, content: String, tag: [String], hidden: Boolean, assign: [String])', 'Problem!',
(arg, ctx) => problem.edit(ctx.domainId, ctx.pdoc.docId, arg), (arg, ctx) => problem.edit(ctx.domainId, ctx.pdoc.docId, arg),
); );
function buildQuery(udoc: User) {
const q: FilterQuery<ProblemDoc> = {
$or: [
{
$and: [
{
$or: [
{ assign: { $size: 0 } },
{ assign: { $in: udoc.group } },
],
},
],
},
{ owner: udoc._id },
{ maintainer: udoc._id },
],
};
if (udoc.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) {
q.$or[0].$and.push({ hidden: false });
}
return q;
}
export class ProblemHandler extends Handler { export class ProblemHandler extends Handler {
async cleanup() { async cleanup() {
if (this.response.template === 'problem_main.html' && this.request.json) { if (this.response.template === 'problem_main.html' && this.request.json) {
@@ -123,7 +150,7 @@ export class ProblemMainHandler extends ProblemHandler {
async get(domainId: string, page = 1, q = '', category = []) { async get(domainId: string, page = 1, q = '', category = []) {
this.response.template = 'problem_main.html'; this.response.template = 'problem_main.html';
// eslint-disable-next-line @typescript-eslint/no-shadow // eslint-disable-next-line @typescript-eslint/no-shadow
const query: FilterQuery<ProblemDoc> = {}; const query = buildQuery(this.user);
const psdict = {}; const psdict = {};
const search = global.Hydro.lib.problemSearch; const search = global.Hydro.lib.problemSearch;
let sort: string[]; let sort: string[];
@@ -145,9 +172,6 @@ export class ProblemMainHandler extends ProblemHandler {
sort = result; sort = result;
} else query.$text = { $search: q }; } else query.$text = { $search: q };
} }
if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) {
query.$or = [{ hidden: false }, { owner: this.user._id }, { maintainer: this.user._id }];
}
await bus.serial('problem/list', query, this); await bus.serial('problem/list', query, this);
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
let [pdocs, ppcount, pcount] = fail let [pdocs, ppcount, pcount] = fail
@@ -160,7 +184,7 @@ export class ProblemMainHandler extends ProblemHandler {
if (sort) pdocs = pdocs.sort((a, b) => sort.indexOf(`${a.domainId}/${a.docId}`) - sort.indexOf(`${b.domainId}/${b.docId}`)); if (sort) pdocs = pdocs.sort((a, b) => sort.indexOf(`${a.domainId}/${a.docId}`) - sort.indexOf(`${b.domainId}/${b.docId}`));
if (q) { if (q) {
const pdoc = await problem.get(domainId, +q || q, problem.PROJECTION_LIST); const pdoc = await problem.get(domainId, +q || q, problem.PROJECTION_LIST);
if (pdoc && (!pdoc.hidden || this.user.own(pdoc) || this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN))) { if (pdoc && problem.canViewBy(pdoc, this.user)) {
const count = pdocs.length; const count = pdocs.length;
pdocs = pdocs.filter((doc) => doc.docId !== pdoc.docId); pdocs = pdocs.filter((doc) => doc.docId !== pdoc.docId);
pdocs.unshift(pdoc); pdocs.unshift(pdoc);
@@ -252,9 +276,8 @@ export class ProblemMainHandler extends ProblemHandler {
export class ProblemRandomHandler extends ProblemHandler { export class ProblemRandomHandler extends ProblemHandler {
@param('category', Types.Name, true, null, parseCategory) @param('category', Types.Name, true, null, parseCategory)
async get(domainId: string, category: string[] = []) { async get(domainId: string, category: string[] = []) {
const q: FilterQuery<ProblemDoc> = category.length ? { $and: [] } : {}; const q = buildQuery(this.user);
for (const tag of category) q.$and.push({ tag }); if (category.length) q.$and = category.map((tag) => ({ tag }));
if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false;
await bus.serial('problem/list', q, this); await bus.serial('problem/list', q, this);
const pid = await problem.random(domainId, q); const pid = await problem.random(domainId, q);
if (!pid) throw new NoProblemError(); if (!pid) throw new NoProblemError();
@@ -284,8 +307,8 @@ export class ProblemDetailHandler extends ProblemHandler {
if (!showAccept) this.pdoc.nAccept = 0; if (!showAccept) this.pdoc.nAccept = 0;
if (contest.isNotStarted(this.tdoc)) throw new ContestNotLiveError(tid); if (contest.isNotStarted(this.tdoc)) throw new ContestNotLiveError(tid);
if (!contest.isDone(this.tdoc) && !this.tsdoc?.attend) throw new ContestNotAttendedError(tid); if (!contest.isDone(this.tdoc) && !this.tsdoc?.attend) throw new ContestNotAttendedError(tid);
} else if (this.pdoc.hidden && !this.user.own(this.pdoc)) { } else if (!problem.canViewBy(this.pdoc, this.user)) {
this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN); throw new PermissionError(PERM.PERM_VIEW_PROBLEM_HIDDEN);
} }
let ddoc = this.domain; let ddoc = this.domain;
if (this.pdoc.reference) { if (this.pdoc.reference) {
@@ -462,13 +485,14 @@ export class ProblemEditHandler extends ProblemManageHandler {
@post('hidden', Types.Boolean) @post('hidden', Types.Boolean)
@post('tag', Types.Content, true, null, parseCategory) @post('tag', Types.Content, true, null, parseCategory)
@post('difficulty', Types.PositiveInt, (i) => +i <= 10, true) @post('difficulty', Types.PositiveInt, (i) => +i <= 10, true)
@post('assign', Types.CommaSeperatedArray)
async post( async post(
domainId: string, pid: string | number, title: string, content: string, domainId: string, pid: string | number, title: string, content: string,
newPid: string = '', hidden = false, tag: string[] = [], difficulty = 0, newPid: string = '', hidden = false, tag: string[] = [], difficulty = 0, assign: string[] = [],
) { ) {
if (newPid !== this.pdoc.pid && await problem.get(domainId, newPid)) throw new BadRequestError('new pid exists'); if (newPid !== this.pdoc.pid && await problem.get(domainId, newPid)) throw new BadRequestError('new pid exists');
const $update: Partial<ProblemDoc> = { const $update: Partial<ProblemDoc> = {
title, content, pid: newPid, hidden, tag: tag ?? [], difficulty, title, content, pid: newPid, hidden, assign, tag: tag ?? [], difficulty,
}; };
let pdoc = await problem.get(domainId, pid); let pdoc = await problem.get(domainId, pid);
pdoc = await problem.edit(domainId, pdoc.docId, $update); pdoc = await problem.edit(domainId, pdoc.docId, $update);
@@ -765,9 +789,13 @@ export class ProblemCreateHandler extends Handler {
@post('hidden', Types.Boolean) @post('hidden', Types.Boolean)
@post('difficulty', Types.PositiveInt, (i) => +i <= 10, true) @post('difficulty', Types.PositiveInt, (i) => +i <= 10, true)
@post('tag', Types.Content, true, null, parseCategory) @post('tag', Types.Content, true, null, parseCategory)
async post(domainId: string, title: string, content: string, pid: string, hidden = false, difficulty = 0, tag: string[] = []) { @post('assign', Types.CommaSeperatedArray)
async post(
domainId: string, title: string, content: string, pid: string,
hidden = false, difficulty = 0, tag: string[] = [], assign: string[] = [],
) {
if (pid && await problem.get(domainId, pid)) throw new BadRequestError('invalid pid'); if (pid && await problem.get(domainId, pid)) throw new BadRequestError('invalid pid');
const docId = await problem.add(domainId, pid, title, content, this.user._id, tag ?? [], hidden); const docId = await problem.add(domainId, pid, title, content, this.user._id, tag ?? [], hidden, assign);
if (difficulty) await problem.edit(domainId, docId, { difficulty }); if (difficulty) await problem.edit(domainId, docId, { difficulty });
this.response.body = { pid: pid || docId }; this.response.body = { pid: pid || docId };
this.response.redirect = this.url('problem_files', { pid: pid || docId }); this.response.redirect = this.url('problem_files', { pid: pid || docId });
+10 -13
View File
@@ -69,13 +69,13 @@ class RecordListHandler extends Handler {
const rdocs = invalid const rdocs = invalid
? [] as RecordDoc[] ? [] as RecordDoc[]
: await cursor.skip((page - 1) * limit).limit(limit).toArray(); : await cursor.skip((page - 1) * limit).limit(limit).toArray();
const canViewProblem = this.user.hasPerm(PERM.PERM_VIEW_PROBLEM); const canViewProblem = tid || this.user.hasPerm(PERM.PERM_VIEW_PROBLEM);
const canViewProblemHidden = (!!tid) || this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN); const canViewHiddenProblem = this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || this.user._id;
const [udict, pdict] = full ? [{}, {}] const [udict, pdict] = full ? [{}, {}]
: await Promise.all([ : await Promise.all([
user.getList(domainId, rdocs.map((rdoc) => rdoc.uid)), user.getList(domainId, rdocs.map((rdoc) => rdoc.uid)),
canViewProblem canViewProblem
? problem.getList(domainId, rdocs.map((rdoc) => rdoc.pid), canViewProblemHidden || this.user._id, false) ? problem.getList(domainId, rdocs.map((rdoc) => rdoc.pid), canViewHiddenProblem, this.user.group, false)
: Object.fromEntries([rdocs.map((rdoc) => [rdoc.pid, { ...problem.default, pid: rdoc.pid }])]), : Object.fromEntries([rdocs.map((rdoc) => [rdoc.pid, { ...problem.default, pid: rdoc.pid }])]),
]); ]);
this.response.body = { this.response.body = {
@@ -132,10 +132,9 @@ class RecordDetailHandler extends Handler {
rdoc.compilerTexts = []; rdoc.compilerTexts = [];
} }
let canView = pdoc && this.user.own(pdoc); if (pdoc && !(rdoc.contest && this.user._id === rdoc.uid)) {
canView ||= !pdoc?.hidden && this.user.hasPerm(PERM.PERM_VIEW_PROBLEM); if (!problem.canViewBy(pdoc, this.user)) throw new PermissionError(PERM.PERM_VIEW_PROBLEM_HIDDEN);
canView ||= !!rdoc.contest || this.user._id === rdoc.uid; }
if (!canView) this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
this.response.body = { this.response.body = {
udoc, rdoc, pdoc, tdoc, udoc, rdoc, pdoc, tdoc,
@@ -250,7 +249,7 @@ class RecordMainConnectionHandler extends ConnectionHandler {
]); ]);
const tdoc = this.tid ? this.tdoc || await contest.get(rdoc.domainId, new ObjectID(this.tid)) : null; const tdoc = this.tid ? this.tdoc || await contest.get(rdoc.domainId, new ObjectID(this.tid)) : null;
if (pdoc && !rdoc.contest) { if (pdoc && !rdoc.contest) {
if (pdoc.hidden && !this.user.own(pdoc) && !this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) pdoc = null; if (problem.canViewBy(pdoc, this.user)) pdoc = null;
if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM)) pdoc = null; if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM)) pdoc = null;
} }
if (this.pretest) this.send({ rdoc }); if (this.pretest) this.send({ rdoc });
@@ -293,11 +292,9 @@ class RecordDetailConnectionHandler extends ConnectionHandler {
rdoc.compilerTexts = []; rdoc.compilerTexts = [];
} }
let canView = pdoc && this.user.own(pdoc); if (!(rdoc.contest && this.user._id === rdoc.uid)) {
canView ||= !pdoc?.hidden && this.user.hasPerm(PERM.PERM_VIEW_PROBLEM); if (!problem.canViewBy(pdoc, this.user)) throw new PermissionError(PERM.PERM_VIEW_PROBLEM_HIDDEN);
canView ||= this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN); }
canView ||= !!rdoc.contest || this.user._id === rdoc.uid;
if (!canView) throw new PermissionError(PERM.PERM_READ_RECORD_CODE);
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));
+3 -2
View File
@@ -97,9 +97,10 @@ class TrainingDetailHandler extends Handler {
const tdoc = await training.get(domainId, tid); const tdoc = await training.get(domainId, tid);
await bus.serial('training/get', tdoc, this); await bus.serial('training/get', tdoc, this);
const pids = training.getPids(tdoc.dag); const pids = training.getPids(tdoc.dag);
const canViewHidden = this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || this.user._id;
const [owner, pdict] = await Promise.all([ const [owner, pdict] = await Promise.all([
user.getById(domainId, tdoc.owner), user.getById(domainId, tdoc.owner),
problem.getList(domainId, pids, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || this.user._id, true), problem.getList(domainId, pids, canViewHidden, this.user.group, true),
]); ]);
const psdict = await problem.getListStatus(domainId, this.user._id, pids); const psdict = await problem.getListStatus(domainId, this.user._id, pids);
const donePids = new Set<number>(); const donePids = new Set<number>();
@@ -119,7 +120,7 @@ class TrainingDetailHandler extends Handler {
for (const node of tdoc.dag) { for (const node of tdoc.dag) {
ndict[node._id] = node; ndict[node._id] = node;
const totalCount = node.pids.length; const totalCount = node.pids.length;
const doneCount = Set.intersection(new Set(node.pids), donePids).size; const doneCount = Set.intersection(node.pids, donePids).size;
const nsdoc = { const nsdoc = {
progress: totalCount ? Math.floor(100 * (doneCount / totalCount)) : 100, progress: totalCount ? Math.floor(100 * (doneCount / totalCount)) : 100,
isDone: training.isDone(node, doneNids, donePids), isDone: training.isDone(node, doneNids, donePids),
+6 -1
View File
@@ -303,7 +303,12 @@ class UserDetailHandler 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;
await Promise.all([domainId, ...(union?.union || [])].map(async (did) => { await Promise.all([domainId, ...(union?.union || [])].map(async (did) => {
const psdocs = await problem.getMultiStatus(did, { uid, status: STATUS.STATUS_ACCEPTED }).toArray(); const psdocs = await problem.getMultiStatus(did, { uid, status: STATUS.STATUS_ACCEPTED }).toArray();
pdocs.push(...Object.values(await problem.getList(did, psdocs.map((i) => i.docId), canViewHidden, false, undefined, true))); pdocs.push(...Object.values(
await problem.getList(
did, psdocs.map((i) => i.docId), canViewHidden,
this.user.group, false, undefined, true,
),
));
})); }));
for (const pdoc of pdocs) { for (const pdoc of pdocs) {
for (const tag of pdoc.tag) { for (const tag of pdoc.tag) {
+24 -22
View File
@@ -97,32 +97,33 @@ export interface GDoc {
export type ownerInfo = { owner: number, maintainer?: number[] }; export type ownerInfo = { owner: number, maintainer?: number[] };
export interface User extends Record<string, any> { export interface User extends Record<string, any> {
_id: number, _id: number;
_udoc: Udoc, _udoc: Udoc;
_dudoc: any, _dudoc: any;
_salt: string, _salt: string;
_hash: string, _hash: string;
_regip: string, _regip: string;
_loginip: string, _loginip: string;
_tfa: string, _tfa: string;
mail: string, mail: string;
uname: string, uname: string;
hashType: string, hashType: string;
priv: number, priv: number;
regat: Date, regat: Date;
loginat: Date, loginat: Date;
perm: bigint, perm: bigint;
scope: bigint, scope: bigint;
role: string, role: string;
tfa: boolean, group?: string[];
tfa: boolean;
own<T extends ownerInfo>(doc: T, checkPerm: bigint): boolean own<T extends ownerInfo>(doc: T, checkPerm: bigint): boolean
own<T extends ownerInfo>(doc: T, exact: boolean): boolean own<T extends ownerInfo>(doc: T, exact: boolean): boolean
own<T extends ownerInfo>(doc: T): boolean own<T extends ownerInfo>(doc: T): boolean
own<T extends { owner: number, maintainer?: number[] }>(doc: T): boolean; own<T extends { owner: number, maintainer?: number[] }>(doc: T): boolean;
hasPerm: (...perm: bigint[]) => boolean, hasPerm: (...perm: bigint[]) => boolean;
hasPriv: (...priv: number[]) => boolean, hasPriv: (...priv: number[]) => boolean;
checkPassword: (password: string) => void, checkPassword: (password: string) => void;
} }
export type Udict = NumericDictionary<User>; export type Udict = NumericDictionary<User>;
@@ -244,7 +245,8 @@ declare module './model/problem' {
tag: string[]; tag: string[];
data: FileInfo[]; data: FileInfo[];
additional_file: FileInfo[]; additional_file: FileInfo[];
hidden: boolean; hidden?: boolean;
assign: string[];
html?: boolean; html?: boolean;
stats?: any; stats?: any;
difficulty?: number; difficulty?: number;
+4 -5
View File
@@ -275,17 +275,16 @@ export function getNodes(domainId: string) {
return document.getMulti(domainId, document.TYPE_DISCUSSION_NODE).toArray(); return document.getMulti(domainId, document.TYPE_DISCUSSION_NODE).toArray();
} }
export async function getListVnodes(domainId: string, ddocs: any, getHidden: boolean) { export async function getListVnodes(domainId: string, ddocs: any, getHidden = false, assign: string[] = []) {
const tasks = [];
const res = {}; const res = {};
async function task(ddoc: DiscussionDoc) { async function task(ddoc: DiscussionDoc) {
const vnode = await getVnode(domainId, ddoc.parentType, ddoc.parentId.toString()); const vnode = await getVnode(domainId, ddoc.parentType, ddoc.parentId.toString());
if (!res[ddoc.parentType]) res[ddoc.parentType] = {}; if (!res[ddoc.parentType]) res[ddoc.parentType] = {};
if (vnode.hidden && !getHidden) return; if (!getHidden && vnode.hidden) return;
if (vnode.assign?.length && Set.intersection(vnode.assign, assign).size) return;
res[ddoc.parentType][ddoc.parentId] = vnode; res[ddoc.parentType][ddoc.parentId] = vnode;
} }
for (const ddoc of ddocs) tasks.push(task(ddoc)); await Promise.all(ddocs.map((ddoc) => task(ddoc)));
await Promise.all(tasks);
return res; return res;
} }
+27 -9
View File
@@ -6,7 +6,7 @@ import type { Readable } from 'stream';
import { streamToBuffer } from '@hydrooj/utils/lib/utils'; import { streamToBuffer } from '@hydrooj/utils/lib/utils';
import { ProblemNotFoundError, ValidationError } from '../error'; import { ProblemNotFoundError, ValidationError } from '../error';
import type { import type {
Document, ProblemDict, ProblemStatusDoc, Document, ProblemDict, ProblemStatusDoc, User,
} from '../interface'; } from '../interface';
import { parseConfig } from '../lib/testdataConfig'; import { parseConfig } from '../lib/testdataConfig';
import * as bus from '../service/bus'; import * as bus from '../service/bus';
@@ -31,7 +31,7 @@ export class ProblemModel {
static PROJECTION_LIST: Field[] = [ static PROJECTION_LIST: Field[] = [
'_id', 'domainId', 'docType', 'docId', 'pid', '_id', 'domainId', 'docType', 'docId', 'pid',
'owner', 'title', 'nSubmit', 'nAccept', 'difficulty', 'owner', 'title', 'nSubmit', 'nAccept', 'difficulty',
'tag', 'hidden', 'stats', 'tag', 'assign', 'stats',
]; ];
static PROJECTION_PUBLIC: Field[] = [ static PROJECTION_PUBLIC: Field[] = [
@@ -57,6 +57,7 @@ export class ProblemModel {
additional_file: [], additional_file: [],
stats: {}, stats: {},
hidden: true, hidden: true,
assign: [],
config: '', config: '',
difficulty: 0, difficulty: 0,
}; };
@@ -78,27 +79,31 @@ export class ProblemModel {
additional_file: [], additional_file: [],
stats: {}, stats: {},
hidden: true, hidden: true,
assign: [],
config: '', config: '',
difficulty: 0, difficulty: 0,
}; };
static async add( static async add(
domainId: string, pid: string = '', title: string, content: string, owner: number, domainId: string, pid: string = '', title: string, content: string, owner: number,
tag: string[] = [], hidden = false, tag: string[] = [], hidden = false, assign: string[] = [],
) { ) {
const [doc] = await ProblemModel.getMulti(domainId, {}) const [doc] = await ProblemModel.getMulti(domainId, {})
.sort({ docId: -1 }).limit(1).project({ docId: 1 }) .sort({ docId: -1 }).limit(1).project({ docId: 1 })
.toArray(); .toArray();
const result = await ProblemModel.addWithId(domainId, (doc?.docId || 0) + 1, pid, title, content, owner, tag, hidden); const result = await ProblemModel.addWithId(
domainId, (doc?.docId || 0) + 1, pid,
title, content, owner, tag, hidden, assign,
);
return result; return result;
} }
static async addWithId( static async addWithId(
domainId: string, docId: number, pid: string = '', title: string, domainId: string, docId: number, pid: string = '', title: string,
content: string, owner: number, tag: string[] = [], hidden = false, content: string, owner: number, tag: string[] = [], hidden = false, assign: string[] = [],
) { ) {
const args: Partial<ProblemDoc> = { const args: Partial<ProblemDoc> = {
title, tag, hidden, nSubmit: 0, nAccept: 0, sort: sortable(pid || `P${docId}`), title, tag, hidden, assign, nSubmit: 0, nAccept: 0, sort: sortable(pid || `P${docId}`),
}; };
if (pid) args.pid = pid; if (pid) args.pid = pid;
await bus.serial('problem/before-add', domainId, content, owner, docId, args); await bus.serial('problem/before-add', domainId, content, owner, docId, args);
@@ -279,8 +284,8 @@ export class ProblemModel {
} }
static async getList( static async getList(
domainId: string, pids: number[], domainId: string, pids: number[], canViewHidden: number | boolean = false,
getHidden: number | boolean = false, doThrow = true, projection = ProblemModel.PROJECTION_PUBLIC, group: string[] = [], doThrow = true, projection = ProblemModel.PROJECTION_PUBLIC,
indexByDocIdOnly = false, indexByDocIdOnly = false,
): Promise<ProblemDict> { ): Promise<ProblemDict> {
const r: Record<number, ProblemDoc> = {}; const r: Record<number, ProblemDoc> = {};
@@ -288,7 +293,12 @@ export class ProblemModel {
const q: any = { $or: [{ docId: { $in: pids } }, { pid: { $in: pids } }] }; const q: any = { $or: [{ docId: { $in: pids } }, { pid: { $in: pids } }] };
let pdocs = await document.getMulti(domainId, document.TYPE_PROBLEM, q) let pdocs = await document.getMulti(domainId, document.TYPE_PROBLEM, q)
.project(buildProjection(projection)).toArray(); .project(buildProjection(projection)).toArray();
if (getHidden !== true) pdocs = pdocs.filter((i) => !i.hidden || i.owner === getHidden); if (group.length > 0) {
pdocs = pdocs.filter((i) => !i.assign?.length || Set.intersection(group, i.assign).size);
}
if (canViewHidden !== true) {
pdocs = pdocs.filter((i) => i.owner === canViewHidden || !i.hidden);
}
for (const pdoc of pdocs) { for (const pdoc of pdocs) {
try { try {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
@@ -352,6 +362,14 @@ export class ProblemModel {
static setStar(domainId: string, pid: number, uid: number, star: boolean) { static setStar(domainId: string, pid: number, uid: number, star: boolean) {
return document.setStatus(domainId, document.TYPE_PROBLEM, pid, uid, { star }); return document.setStatus(domainId, document.TYPE_PROBLEM, pid, uid, { star });
} }
static canViewBy(pdoc: ProblemDoc, udoc: User) {
if (!udoc.hasPerm(PERM.PERM_VIEW_PROBLEM)) return false;
if (udoc.own(pdoc)) return true;
if (pdoc.hidden && !udoc.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) return false;
if (!pdoc.assign.length) return true;
return !!Set.intersection(pdoc.assign, udoc.group).size;
}
} }
bus.on('problem/addTestdata', async (domainId, docId, name) => { bus.on('problem/addTestdata', async (domainId, docId, name) => {
+1 -1
View File
@@ -158,7 +158,7 @@ bus.once('app/started', async () => {
while (true) { while (true) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const res = await collEvent.findOneAndUpdate( const res = await collEvent.findOneAndUpdate(
{ ack: { $not: { $elemMatch: { $eq: id } } } }, { ack: { $nin: [id] } },
{ $push: { ack: id } }, { $push: { ack: id } },
); );
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
+4 -4
View File
@@ -67,6 +67,7 @@ class User implements _User {
scope: bigint; scope: bigint;
_files: FileInfo[]; _files: FileInfo[];
tfa: boolean; tfa: boolean;
group?: string[];
[key: string]: any; [key: string]: any;
constructor(udoc: Udoc, dudoc, scope = PERM.PERM_ALL) { constructor(udoc: Udoc, dudoc, scope = PERM.PERM_ALL) {
@@ -91,6 +92,7 @@ class User implements _User {
this.scope = typeof scope === 'string' ? BigInt(scope) : scope; this.scope = typeof scope === 'string' ? BigInt(scope) : scope;
this.role = dudoc.role || 'default'; this.role = dudoc.role || 'default';
this.tfa = !!udoc.tfa; this.tfa = !!udoc.tfa;
if (dudoc.group) this.group = [...dudoc.group, this._id.toString()];
for (const key in setting.SETTINGS_BY_KEY) { for (const key in setting.SETTINGS_BY_KEY) {
this[key] = udoc[key] ?? (setting.SETTINGS_BY_KEY[key].value || system.get(`preference.${key}`)); this[key] = udoc[key] ?? (setting.SETTINGS_BY_KEY[key].value || system.get(`preference.${key}`));
@@ -175,6 +177,8 @@ class UserModel {
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 = await domain.getDomainUser(domainId, udoc);
const groups = await UserModel.listGroup(domainId, _id);
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();
cache.set(`id/${res._id}/${domainId}`, res); cache.set(`id/${res._id}/${domainId}`, res);
@@ -373,10 +377,6 @@ class UserModel {
]); ]);
} }
static createGroup(domainId: string, name: string, uids: number[]) {
return collGroup.insertOne({ domainId, name, uids });
}
static async listGroup(domainId: string, uid?: number) { static async listGroup(domainId: string, uid?: number) {
const groups = await collGroup.find(uid ? { domainId, uids: uid } : { domainId }).toArray(); const groups = await collGroup.find(uid ? { domainId, uids: uid } : { domainId }).toArray();
if (uid) { if (uid) {
+5
View File
@@ -84,6 +84,7 @@ export interface Types {
Range: (range: Array<string | number> | Dictionary<any>) => Type, Range: (range: Array<string | number> | Dictionary<any>) => Type,
Array: Type, Array: Type,
NumericArray: Type, NumericArray: Type,
CommaSeperatedArray: Type,
Set: Type, Set: Type,
} }
@@ -163,6 +164,10 @@ export const Types: Types = {
if (v instanceof Array) return !v.map(Number).includes(NaN); if (v instanceof Array) return !v.map(Number).includes(NaN);
return !Number.isNaN(+v); return !Number.isNaN(+v);
}], }],
CommaSeperatedArray: [
(v) => v.toString().replace(/,/g, ',').split(',').map((e) => e.trim()).filter((i) => i),
(v) => v.toString(),
],
Set: [(v) => { Set: [(v) => {
if (v instanceof Array) return new Set(v); if (v instanceof Array) return new Set(v);
return v ? new Set([v]) : new Set(); return v ? new Set([v]) : new Set();
+8
View File
@@ -664,6 +664,14 @@ const scripts: UpgradeScript[] = [
await db.collection('oplog').deleteMany({ type: 'user.login' }); await db.collection('oplog').deleteMany({ type: 'user.login' });
return true; return true;
}, },
async function _57_58() {
const _FRESH_INSTALL_IGNORE = 1;
await db.collection('document').updateMany(
{ docType: document.TYPE_PROBLEM, assign: null },
{ $set: { assign: [] } },
);
return true;
},
]; ];
export default scripts; export default scripts;
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@hydrooj/ui-default", "name": "@hydrooj/ui-default",
"version": "4.27.17", "version": "4.27.18",
"author": "undefined <i@undefined.moe>", "author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "hydro.js", "main": "hydro.js",
+5 -3
View File
@@ -27,7 +27,7 @@ declare global {
} }
interface SetConstructor { interface SetConstructor {
isSuperset: (set: Set<any>, subset: Set<any>) => boolean; isSuperset: (set: Set<any>, subset: Set<any>) => boolean;
intersection: <T>(setA: Set<T>, setB: Set<T>) => Set<T>; intersection: <T>(setA: Set<T> | Array<T>, setB: Set<T> | Array<T>) => Set<T>;
union: <T>(setA: Set<T>, setB: Set<T>) => Set<T>; union: <T>(setA: Set<T>, setB: Set<T>) => Set<T>;
} }
} }
@@ -130,9 +130,11 @@ Set.union = function Union<T>(setA: Set<T>, setB: Set<T>) {
return union; return union;
}; };
Set.intersection = function Intersection<T>(setA: Set<T>, setB: Set<T>) { Set.intersection = function Intersection<T>(A: Set<T> | Array<T> = [], B: Set<T> | Array<T> = []) {
const intersection = new Set<T>(); const intersection = new Set<T>();
for (const elem of setB) if (setA.has(elem)) intersection.add(elem); if (A instanceof Array) A = new Set(A);
if (B instanceof Array) B = new Set(B);
for (const elem of B) if (A.has(elem)) intersection.add(elem);
return intersection; return intersection;
}; };
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@hydrooj/utils", "name": "@hydrooj/utils",
"version": "1.1.10", "version": "1.1.11",
"description": "hydrooj utils", "description": "hydrooj utils",
"main": "package.json", "main": "package.json",
"repository": "https://github.com/hydro-dev/Hydro.git", "repository": "https://github.com/hydro-dev/Hydro.git",