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 Claim Homework: 登录后认领作业
|
||||
Login to Create a Discussion: 登录后创建讨论
|
||||
Login to join training plan: 登录以参加训练计划
|
||||
Login to Submit: 登录后递交
|
||||
Login: 登录
|
||||
Logout All Sessions: 注销所有会话
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydrooj",
|
||||
"version": "3.2.43",
|
||||
"version": "3.3.0",
|
||||
"bin": "bin/hydrooj.js",
|
||||
"main": "src/loader",
|
||||
"module": "src/loader",
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user