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 Claim Homework: 登录后认领作业
Login to Create a Discussion: 登录后创建讨论
Login to join training plan: 登录以参加训练计划
Login to Submit: 登录后递交
Login: 登录
Logout All Sessions: 注销所有会话
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hydrooj",
"version": "3.2.43",
"version": "3.3.0",
"bin": "bin/hydrooj.js",
"main": "src/loader",
"module": "src/loader",
+19 -5
View File
@@ -3,7 +3,7 @@ import moment from 'moment-timezone';
import { ObjectID } from 'mongodb';
import { Time } from '@hydrooj/utils/lib/utils';
import {
ContestNotFoundError, ContestNotLiveError, InvalidTokenError,
ContestNotFoundError, ContestNotLiveError, ForbiddenError, InvalidTokenError,
PermissionError, ValidationError,
} from '../error';
import { Tdoc } from '../interface';
@@ -74,6 +74,18 @@ export class ContestListHandler 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('page', Types.PositiveInt, true)
async get(domainId: string, tid: ObjectID, page = 1) {
@@ -238,9 +250,11 @@ export class ContestEditHandler extends Handler {
@param('rated', Types.Boolean)
@param('code', Types.String, true)
@param('autoHide', Types.String, true)
@param('assign', Types.Array, true)
async post(
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);
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();
if (beginAtMoment.isSameOrAfter(endAt)) throw new ValidationError('duration');
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) {
await contest.edit(domainId, tid, {
title, content, rule, beginAt, endAt, pids, rated,
@@ -267,13 +281,13 @@ export class ContestEditHandler extends Handler {
await TaskModel.deleteMany(task);
if (Date.now() <= endAt.getTime() && autoHide) {
// 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({
...task,
executeAfter: endAt,
});
}
await contest.edit(domainId, tid, { _code, autoHide });
await contest.edit(domainId, tid, { assign, _code, autoHide });
this.response.body = { 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(iceboy): continuation based pagination.
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) {
this.ddoc.parentType = this.ddoc.parentType || this.vnode.type;
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 [vndict, vnodes] = await Promise.all([
discussion.getListVnodes(
domainId, ddocs, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN),
),
discussion.getListVnodes(domainId, ddocs, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN), this.user.group),
discussion.getNodes(domainId),
]);
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 {
DomainJoinAlreadyMemberError, DomainJoinForbiddenError, ForbiddenError,
InvalidJoinInvitationCodeError, RoleAlreadyExistError, ValidationError,
InvalidJoinInvitationCodeError, PermissionError, RoleAlreadyExistError, ValidationError,
} from '../error';
import type { DomainDoc } from '../interface';
import avatar from '../lib/avatar';
@@ -19,6 +19,40 @@ import {
Handler, param, post, query, Route, Types,
} from '../service/server';
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 {
@query('page', Types.PositiveInt, true)
@@ -184,7 +218,7 @@ class DomainRoleHandler extends ManageHandler {
@param('roles', Types.Array)
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');
}
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 {
joinSettings: any;
noCheckPermView = true;
@@ -283,6 +336,7 @@ export async function apply() {
Route('domain_user', '/domain/user', DomainUserHandler);
Route('domain_permission', '/domain/permission', DomainPermissionHandler);
Route('domain_role', '/domain/role', DomainRoleHandler);
Route('domain_group', '/domain/group', DomainUserGroupHandler);
Route('domain_join_applications', '/domain/join_applications', DomainJoinApplicationsHandler);
Route('domain_join', '/domain/join', DomainJoinHandler, 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) {
if (this.user.hasPerm(PERM.PERM_VIEW_DISCUSSION)) {
const ddocs = await discussion.getMulti(domainId).limit(limit).toArray();
const vndict = await discussion.getListVnodes(
domainId, ddocs, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN),
);
const vndict = await discussion.getListVnodes(domainId, ddocs, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN), this.user.group);
this.collectUser(ddocs.map((ddoc) => ddoc.owner));
return ['discussion', ddocs, vndict];
}
@@ -103,7 +101,7 @@ class HomeHandler extends Handler {
for (const psdoc of psdocs) psdict[psdoc.docId] = psdoc;
const pdict = await ProblemModel.getList(
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]);
return ['problem', pdocs, psdict];
+14 -2
View File
@@ -4,7 +4,8 @@ import moment from 'moment-timezone';
import { ObjectID } from 'mongodb';
import { Time } from '@hydrooj/utils/lib/utils';
import {
ContestNotFoundError, HomeworkNotLiveError, ValidationError,
ContestNotFoundError, ForbiddenError, HomeworkNotLiveError,
ValidationError,
} from '../error';
import { PenaltyRules } from '../interface';
import paginate from '../lib/paginate';
@@ -40,6 +41,17 @@ class HomeworkMainHandler 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('page', Types.PositiveInt, true)
async get(domainId: string, tid: ObjectID, page = 1) {
@@ -156,7 +168,7 @@ class HomeworkEditHandler extends Handler {
const endAt = penaltySince.clone().add(extensionDays, 'days');
if (beginAt.isSameOrAfter(penaltySince)) throw new ValidationError('endAtDate', 'endAtTime');
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) {
tid = await contest.add(domainId, title, content, this.user._id,
'homework', beginAt.toDate(), endAt.toDate(), pids, rated,
+52 -24
View File
@@ -1,17 +1,18 @@
import AdmZip from 'adm-zip';
import { statSync } from 'fs-extra';
import { flatten, intersection, isSafeInteger } from 'lodash';
import { intersection, isSafeInteger } from 'lodash';
import { FilterQuery, ObjectID } from 'mongodb';
import { sortFiles } from '@hydrooj/utils/lib/utils';
import {
BadRequestError, ContestNotAttendedError, ContestNotEndedError,
ContestNotFoundError, ContestNotLiveError,
ForbiddenError, NoProblemError, NotFoundError,
PermissionError, ProblemNotFoundError, SolutionNotFoundError, ValidationError,
PermissionError, ProblemNotFoundError, SolutionNotFoundError,
ValidationError,
} from '../error';
import {
ProblemConfig,
ProblemDoc, ProblemStatusDoc, Tdoc, User,
ProblemConfig, ProblemDoc, ProblemStatusDoc,
Tdoc, User,
} from '../interface';
import paginate from '../lib/paginate';
import { isPid, parsePid as convertPid } from '../lib/validator';
@@ -32,7 +33,7 @@ import {
} from '../service/server';
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);
registerValue('FileInfo', [
@@ -58,9 +59,7 @@ registerValue('Problem', [
['difficulty', 'Int'],
['tag', '[String]'],
['hidden', 'Boolean'],
]);
registerValue('ProblemManage', [
['delete', 'Boolean!'],
['assign', '[String]'],
]);
registerResolver(
@@ -68,7 +67,12 @@ registerResolver(
async (arg, ctx) => {
const pdoc = await problem.get(ctx.domainId, arg.pid || arg.id);
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;
return pdoc;
},
@@ -89,10 +93,33 @@ 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),
);
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 {
async cleanup() {
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 = []) {
this.response.template = 'problem_main.html';
// eslint-disable-next-line @typescript-eslint/no-shadow
const query: FilterQuery<ProblemDoc> = {};
const query = buildQuery(this.user);
const psdict = {};
const search = global.Hydro.lib.problemSearch;
let sort: string[];
@@ -145,9 +172,6 @@ export class ProblemMainHandler extends ProblemHandler {
sort = result;
} 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);
// eslint-disable-next-line prefer-const
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 (q) {
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;
pdocs = pdocs.filter((doc) => doc.docId !== pdoc.docId);
pdocs.unshift(pdoc);
@@ -252,9 +276,8 @@ export class ProblemMainHandler extends ProblemHandler {
export class ProblemRandomHandler extends ProblemHandler {
@param('category', Types.Name, true, null, parseCategory)
async get(domainId: string, category: string[] = []) {
const q: FilterQuery<ProblemDoc> = category.length ? { $and: [] } : {};
for (const tag of category) q.$and.push({ tag });
if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false;
const q = buildQuery(this.user);
if (category.length) q.$and = category.map((tag) => ({ tag }));
await bus.serial('problem/list', q, this);
const pid = await problem.random(domainId, q);
if (!pid) throw new NoProblemError();
@@ -284,8 +307,8 @@ export class ProblemDetailHandler extends ProblemHandler {
if (!showAccept) this.pdoc.nAccept = 0;
if (contest.isNotStarted(this.tdoc)) throw new ContestNotLiveError(tid);
if (!contest.isDone(this.tdoc) && !this.tsdoc?.attend) throw new ContestNotAttendedError(tid);
} else if (this.pdoc.hidden && !this.user.own(this.pdoc)) {
this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
} else if (!problem.canViewBy(this.pdoc, this.user)) {
throw new PermissionError(PERM.PERM_VIEW_PROBLEM_HIDDEN);
}
let ddoc = this.domain;
if (this.pdoc.reference) {
@@ -462,13 +485,14 @@ export class ProblemEditHandler extends ProblemManageHandler {
@post('hidden', Types.Boolean)
@post('tag', Types.Content, true, null, parseCategory)
@post('difficulty', Types.PositiveInt, (i) => +i <= 10, true)
@post('assign', Types.CommaSeperatedArray)
async post(
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');
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);
pdoc = await problem.edit(domainId, pdoc.docId, $update);
@@ -765,9 +789,13 @@ export class ProblemCreateHandler extends Handler {
@post('hidden', Types.Boolean)
@post('difficulty', Types.PositiveInt, (i) => +i <= 10, true)
@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');
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 });
this.response.body = { 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
? [] as RecordDoc[]
: await cursor.skip((page - 1) * limit).limit(limit).toArray();
const canViewProblem = this.user.hasPerm(PERM.PERM_VIEW_PROBLEM);
const canViewProblemHidden = (!!tid) || this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
const canViewProblem = tid || this.user.hasPerm(PERM.PERM_VIEW_PROBLEM);
const canViewHiddenProblem = this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || this.user._id;
const [udict, pdict] = full ? [{}, {}]
: await Promise.all([
user.getList(domainId, rdocs.map((rdoc) => rdoc.uid)),
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 }])]),
]);
this.response.body = {
@@ -132,10 +132,9 @@ class RecordDetailHandler extends Handler {
rdoc.compilerTexts = [];
}
let canView = pdoc && this.user.own(pdoc);
canView ||= !pdoc?.hidden && this.user.hasPerm(PERM.PERM_VIEW_PROBLEM);
canView ||= !!rdoc.contest || this.user._id === rdoc.uid;
if (!canView) this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
if (pdoc && !(rdoc.contest && this.user._id === rdoc.uid)) {
if (!problem.canViewBy(pdoc, this.user)) throw new PermissionError(PERM.PERM_VIEW_PROBLEM_HIDDEN);
}
this.response.body = {
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;
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.pretest) this.send({ rdoc });
@@ -293,11 +292,9 @@ class RecordDetailConnectionHandler extends ConnectionHandler {
rdoc.compilerTexts = [];
}
let canView = pdoc && this.user.own(pdoc);
canView ||= !pdoc?.hidden && this.user.hasPerm(PERM.PERM_VIEW_PROBLEM);
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);
if (!(rdoc.contest && this.user._id === rdoc.uid)) {
if (!problem.canViewBy(pdoc, this.user)) throw new PermissionError(PERM.PERM_VIEW_PROBLEM_HIDDEN);
}
this.rid = rid.toString();
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);
await bus.serial('training/get', tdoc, this);
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([
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 donePids = new Set<number>();
@@ -119,7 +120,7 @@ class TrainingDetailHandler extends Handler {
for (const node of tdoc.dag) {
ndict[node._id] = node;
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 = {
progress: totalCount ? Math.floor(100 * (doneCount / totalCount)) : 100,
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;
await Promise.all([domainId, ...(union?.union || [])].map(async (did) => {
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 tag of pdoc.tag) {
+24 -22
View File
@@ -97,32 +97,33 @@ export interface GDoc {
export type ownerInfo = { owner: number, maintainer?: number[] };
export interface User extends Record<string, any> {
_id: number,
_udoc: Udoc,
_dudoc: any,
_salt: string,
_hash: string,
_regip: string,
_loginip: string,
_tfa: string,
_id: number;
_udoc: Udoc;
_dudoc: any;
_salt: string;
_hash: string;
_regip: string;
_loginip: string;
_tfa: string;
mail: string,
uname: string,
hashType: string,
priv: number,
regat: Date,
loginat: Date,
perm: bigint,
scope: bigint,
role: string,
tfa: boolean,
mail: string;
uname: string;
hashType: string;
priv: number;
regat: Date;
loginat: Date;
perm: bigint;
scope: bigint;
role: string;
group?: string[];
tfa: 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): boolean
own<T extends { owner: number, maintainer?: number[] }>(doc: T): boolean;
hasPerm: (...perm: bigint[]) => boolean,
hasPriv: (...priv: number[]) => boolean,
checkPassword: (password: string) => void,
hasPerm: (...perm: bigint[]) => boolean;
hasPriv: (...priv: number[]) => boolean;
checkPassword: (password: string) => void;
}
export type Udict = NumericDictionary<User>;
@@ -244,7 +245,8 @@ declare module './model/problem' {
tag: string[];
data: FileInfo[];
additional_file: FileInfo[];
hidden: boolean;
hidden?: boolean;
assign: string[];
html?: boolean;
stats?: any;
difficulty?: number;
+4 -5
View File
@@ -275,17 +275,16 @@ export function getNodes(domainId: string) {
return document.getMulti(domainId, document.TYPE_DISCUSSION_NODE).toArray();
}
export async function getListVnodes(domainId: string, ddocs: any, getHidden: boolean) {
const tasks = [];
export async function getListVnodes(domainId: string, ddocs: any, getHidden = false, assign: string[] = []) {
const res = {};
async function task(ddoc: DiscussionDoc) {
const vnode = await getVnode(domainId, ddoc.parentType, ddoc.parentId.toString());
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;
}
for (const ddoc of ddocs) tasks.push(task(ddoc));
await Promise.all(tasks);
await Promise.all(ddocs.map((ddoc) => task(ddoc)));
return res;
}
+27 -9
View File
@@ -6,7 +6,7 @@ import type { Readable } from 'stream';
import { streamToBuffer } from '@hydrooj/utils/lib/utils';
import { ProblemNotFoundError, ValidationError } from '../error';
import type {
Document, ProblemDict, ProblemStatusDoc,
Document, ProblemDict, ProblemStatusDoc, User,
} from '../interface';
import { parseConfig } from '../lib/testdataConfig';
import * as bus from '../service/bus';
@@ -31,7 +31,7 @@ export class ProblemModel {
static PROJECTION_LIST: Field[] = [
'_id', 'domainId', 'docType', 'docId', 'pid',
'owner', 'title', 'nSubmit', 'nAccept', 'difficulty',
'tag', 'hidden', 'stats',
'tag', 'assign', 'stats',
];
static PROJECTION_PUBLIC: Field[] = [
@@ -57,6 +57,7 @@ export class ProblemModel {
additional_file: [],
stats: {},
hidden: true,
assign: [],
config: '',
difficulty: 0,
};
@@ -78,27 +79,31 @@ export class ProblemModel {
additional_file: [],
stats: {},
hidden: true,
assign: [],
config: '',
difficulty: 0,
};
static async add(
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, {})
.sort({ docId: -1 }).limit(1).project({ docId: 1 })
.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;
}
static async addWithId(
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> = {
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;
await bus.serial('problem/before-add', domainId, content, owner, docId, args);
@@ -279,8 +284,8 @@ export class ProblemModel {
}
static async getList(
domainId: string, pids: number[],
getHidden: number | boolean = false, doThrow = true, projection = ProblemModel.PROJECTION_PUBLIC,
domainId: string, pids: number[], canViewHidden: number | boolean = false,
group: string[] = [], doThrow = true, projection = ProblemModel.PROJECTION_PUBLIC,
indexByDocIdOnly = false,
): Promise<ProblemDict> {
const r: Record<number, ProblemDoc> = {};
@@ -288,7 +293,12 @@ export class ProblemModel {
const q: any = { $or: [{ docId: { $in: pids } }, { pid: { $in: pids } }] };
let pdocs = await document.getMulti(domainId, document.TYPE_PROBLEM, q)
.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) {
try {
// 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) {
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) => {
+1 -1
View File
@@ -158,7 +158,7 @@ bus.once('app/started', async () => {
while (true) {
// eslint-disable-next-line no-await-in-loop
const res = await collEvent.findOneAndUpdate(
{ ack: { $not: { $elemMatch: { $eq: id } } } },
{ ack: { $nin: [id] } },
{ $push: { ack: id } },
);
// eslint-disable-next-line no-await-in-loop
+4 -4
View File
@@ -67,6 +67,7 @@ class User implements _User {
scope: bigint;
_files: FileInfo[];
tfa: boolean;
group?: string[];
[key: string]: any;
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.role = dudoc.role || 'default';
this.tfa = !!udoc.tfa;
if (dudoc.group) this.group = [...dudoc.group, this._id.toString()];
for (const key in setting.SETTINGS_BY_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 });
if (!udoc) return null;
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);
const res = await new User(udoc, dudoc, scope).init();
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) {
const groups = await collGroup.find(uid ? { domainId, uids: uid } : { domainId }).toArray();
if (uid) {
+5
View File
@@ -84,6 +84,7 @@ export interface Types {
Range: (range: Array<string | number> | Dictionary<any>) => Type,
Array: Type,
NumericArray: Type,
CommaSeperatedArray: Type,
Set: Type,
}
@@ -163,6 +164,10 @@ export const Types: Types = {
if (v instanceof Array) return !v.map(Number).includes(NaN);
return !Number.isNaN(+v);
}],
CommaSeperatedArray: [
(v) => v.toString().replace(/,/g, ',').split(',').map((e) => e.trim()).filter((i) => i),
(v) => v.toString(),
],
Set: [(v) => {
if (v instanceof Array) return new Set(v);
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' });
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;