mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
core: feat: problem group-assign
This commit is contained in:
1 parent
f699d67019
commit
8fd23e6cea
21 files changed
+250
-104
No files matched your search
@@ -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,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",
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,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",
|
||||||
|
|||||||
@@ -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,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",
|
||||||
|
|||||||
Reference in new issue
Block a user