core: feat: problem_delete

This commit is contained in:
undefined committed 2021-01-11 23:01:00 +08:00
1 parent 3d9acf2c1a
commit 1543a374e3
12 files changed
+151 -174

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hydrooj",
"version": "2.15.16",
"version": "2.15.17",
"bin": "bin/hydrooj.js",
"main": "dist/loader.js",
"typings": "dist/loader.d.ts",
+2 -21
View File
@@ -73,16 +73,14 @@ class RecordDetailHandler extends RecordHandler {
const tdoc = await contest.get(domainId, rdoc.contest.tid, rdoc.contest.type);
if (!this.canShowRecord(tdoc, true)) throw new PermissionError(rid);
}
if (rdoc.uid !== this.user.uid && !this.user.hasPerm(PERM.PERM_READ_RECORD_CODE)) {
rdoc.code = null;
}
if (rdoc.uid !== this.user.uid && !this.user.hasPerm(PERM.PERM_READ_RECORD_CODE)) rdoc.code = null;
// eslint-disable-next-line prefer-const
let [pdoc, udoc] = await Promise.all([
problem.get(domainId, rdoc.pid),
user.getById(domainId, rdoc.uid),
]);
if (!rdoc.contest && pdoc.hidden && pdoc.owner !== this.user._id) {
if (this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) {
if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) {
pdoc = problem.Pdoc.create(pdoc.docId, pdoc.pid);
}
}
@@ -130,22 +128,6 @@ class RecordDetailHandler extends RecordHandler {
}
}
/**
* @deprecated
* use RecordDetailHandler.postRejudge instead.
*/
class RecordRejudgeHandler extends Handler {
@param('rid', Types.ObjectID)
async post(domainId: string, rid: ObjectID) {
const rdoc = await record.get(domainId, rid);
if (rdoc) {
await record.reset(domainId, rid, true);
await record.judge(domainId, rid, 0);
}
this.back();
}
}
const RecordConnectionHandler = contest.ContestHandlerMixin(ConnectionHandler);
class RecordMainConnectionHandler extends RecordConnectionHandler {
@@ -230,7 +212,6 @@ class RecordDetailConnectionHandler extends contest.ContestHandlerMixin(Connecti
export async function apply() {
Route('record_main', '/record', RecordListHandler);
Route('record_detail', '/record/:rid', RecordDetailHandler);
Route('record_rejudge', '/record/:rid/rejudge', RecordRejudgeHandler);
Connection('record_conn', '/record-conn', RecordMainConnectionHandler);
Connection('record_detail_conn', '/record-detail-conn', RecordDetailConnectionHandler);
}
+1
View File
@@ -33,6 +33,7 @@ export interface SystemKeys {
'server.worker': number,
'server.port': number,
'server.language': string,
'problem.categories': string,
'session.keys': string[],
'session.secure': boolean,
'session.expire_seconds': number,
+6 -16
View File
@@ -4,11 +4,6 @@ import * as bus from '../service/bus';
const coll = db.collection('blacklist');
export async function add(ip: string) {
/**
* Add a ip into blacklist.
* @param {string} ip
* @returns {Promise<Bdoc>}
*/
const expireAt = new Date(new Date().getTime() + 365 * 24 * 60 * 60 * 1000);
const res = await coll.findOneAndUpdate(
{ _id: ip },
@@ -18,21 +13,16 @@ export async function add(ip: string) {
return res.value;
}
export function get(ip: string) {
/**
* Get a ip, return null if not.
* @param {string} ip
* @returns {Promise<Bdoc>}
*/
return coll.findOne({ _id: ip });
export async function get(ip: string) {
return await coll.findOne({ _id: ip });
}
export function del(ip: string) {
return coll.deleteOne({ _id: ip });
export async function del(ip: string) {
return await coll.deleteOne({ _id: ip });
}
function ensureIndexes() {
return coll.createIndex('expireAt', { expireAfterSeconds: 0 });
async function ensureIndexes() {
return await coll.createIndex('expireAt', { expireAfterSeconds: 0 });
}
bus.once('app/started', ensureIndexes);
+17
View File
@@ -5,6 +5,7 @@ import * as training from './training';
import * as document from './document';
import { DocumentNotFoundError } from '../error';
import { Ddoc, Drdoc, Drrdoc } from '../interface';
import * as bus from '../service/bus';
export const typeDisplay = {
[document.TYPE_PROBLEM]: 'problem',
@@ -215,6 +216,22 @@ export async function getListVnodes(domainId: string, ddocs: any, getHidden: boo
return res;
}
bus.on('problem/delete', async (domainId, docId) => {
const dids = await document.getMulti(
domainId, document.TYPE_DISCUSSION,
{ parentType: document.TYPE_PROBLEM, parentId: docId },
).project({ docId: 1 }).map((ddoc) => ddoc.docId).toArray();
const drids = await document.getMulti(
domainId, document.TYPE_DISCUSSION_REPLY,
{ parentType: document.TYPE_DISCUSSION, parentId: { $in: dids } },
).project({ docId: 1 }).map((drdoc) => drdoc.docId).toArray();
return await Promise.all([
document.deleteMultiStatus(domainId, document.TYPE_DISCUSSION, { docId: { $in: dids } }),
document.deleteMulti(domainId, document.TYPE_DISCUSSION, { docId: { $in: dids } }),
document.deleteMulti(domainId, document.TYPE_DISCUSSION_REPLY, { docId: { $in: drids } }),
]);
});
global.Hydro.model.discussion = {
typeDisplay,
add,
-3
View File
@@ -25,7 +25,6 @@ export const TYPE_DISCUSSION = 21;
export const TYPE_DISCUSSION_REPLY = 22;
export const TYPE_CONTEST = 30;
export const TYPE_TRAINING = 40;
export const TYPE_FILE = 50;
export const TYPE_HOMEWORK = 60;
export interface DocType {
@@ -37,7 +36,6 @@ export interface DocType {
[TYPE_DISCUSSION_REPLY]: Drdoc,
[TYPE_CONTEST]: Tdoc,
[TYPE_TRAINING]: TrainingDoc,
[TYPE_FILE]: any,
[TYPE_HOMEWORK]: Tdoc,
}
@@ -457,6 +455,5 @@ global.Hydro.model.document = {
TYPE_PROBLEM,
TYPE_PROBLEM_LIST,
TYPE_PROBLEM_SOLUTION,
TYPE_FILE,
TYPE_TRAINING,
};
+6
View File
@@ -9,6 +9,7 @@ import { Content } from '../loader';
import { ArrayKeys, NumberKeys, Projection } from '../typeutils';
import { ProblemNotFoundError } from '../error';
import storage from '../service/storage';
import * as bus from '../service/bus';
export interface Pdoc {
_id: ObjectID
@@ -207,6 +208,11 @@ export function setStar(domainId: string, pid: number, uid: number, star: boolea
return document.setStatus(domainId, document.TYPE_PROBLEM, pid, uid, { star });
}
bus.on('problem/delete', (domainId, docId) => Promise.all([
document.deleteOne(domainId, document.TYPE_PROBLEM, docId),
document.deleteMultiStatus(domainId, document.TYPE_PROBLEM, { docId }),
]));
global.Hydro.model.problem = {
Pdoc,
+19
View File
@@ -2,12 +2,15 @@ import {
ObjectID, Collection, UpdateQuery, PushOperator, MatchKeysAndValues, OnlyFieldsOfType,
} from 'mongodb';
import { Dictionary } from 'lodash';
import moment from 'moment';
import { STATUS } from './builtin';
import * as task from './task';
import * as problem from './problem';
import {
Rdoc, TestCase, RunConfig, ContestInfo, ProblemConfig,
} from '../interface';
import { Time } from '../utils';
import * as bus from '../service/bus';
import db from '../service/db';
import storage from '../service/storage';
@@ -55,6 +58,19 @@ export async function get(domainId: string, _id: ObjectID): Promise<Rdoc | null>
return null;
}
export async function stat(domainId?: string) {
const [d5min, d1h, d1d, d1m, total] = await Promise.all([
coll.find({ _id: { $gte: Time.getObjectID(moment().add(-5, 'minutes')) }, ...domainId ? { domainId } : {} }).count(),
coll.find({ _id: { $gte: Time.getObjectID(moment().add(-1, 'hour')) }, ...domainId ? { domainId } : {} }).count(),
coll.find({ _id: { $gte: Time.getObjectID(moment().add(-1, 'day')) }, ...domainId ? { domainId } : {} }).count(),
coll.find({ _id: { $gte: Time.getObjectID(moment().add(-1, 'month')) }, ...domainId ? { domainId } : {} }).count(),
coll.find(domainId ? { domainId } : {}).count(),
]);
return {
d5min, d1h, d1d, d1m, total,
};
}
export async function judge(domainId: string, rid: ObjectID, priority = 1) {
const rdoc = await get(domainId, rid);
let config: RunConfig | ProblemConfig = rdoc.config;
@@ -186,11 +202,14 @@ export function getByUid(domainId: string, uid: number, limit: number): Promise<
return coll.find({ domainId, uid }).limit(limit).toArray();
}
bus.on('problem/delete', (domainId, docId) => coll.deleteMany({ domainId, pid: docId }));
global.Hydro.model.record = {
coll,
add,
get,
getMulti,
stat,
update,
count,
reset,
+17 -2
View File
@@ -1,6 +1,7 @@
import { ObjectID } from 'mongodb';
import * as document from './document';
import { SolutionNotFoundError } from '../error';
import * as bus from '../service/bus';
export function add(domainId: string, pid: number, owner: number, content: string) {
return document.add(
@@ -26,8 +27,11 @@ export function edit(domainId: string, psid: ObjectID, content: string) {
return document.set(domainId, document.TYPE_PROBLEM_SOLUTION, psid, { content });
}
export function del(domainId: string, psid: ObjectID) {
return document.deleteOne(domainId, document.TYPE_PROBLEM_SOLUTION, psid);
export async function del(domainId: string, psid: ObjectID) {
return await Promise.all([
document.deleteOne(domainId, document.TYPE_PROBLEM_SOLUTION, psid),
document.deleteMultiStatus(domainId, document.TYPE_PROBLEM_SOLUTION, { docId: psid }),
]);
}
export function count(domainId: string, query: any) {
@@ -75,6 +79,17 @@ export async function getListStatus(domainId: string, psids: ObjectID[], uid: nu
return result;
}
bus.on('problem/delete', async (domainId, docId) => {
const psids = await document.getMulti(
domainId, document.TYPE_PROBLEM_SOLUTION,
{ parentType: document.TYPE_PROBLEM, parentId: docId },
).project({ docId: 1 }).map((psdoc) => psdoc.docId).toArray();
return await Promise.all([
document.deleteMulti(domainId, document.TYPE_PROBLEM_SOLUTION, { docId: { $in: psids } }),
document.deleteMultiStatus(domainId, document.TYPE_PROBLEM_SOLUTION, { docId: { $in: psids } }),
]);
});
global.Hydro.model.solution = {
count,
add,
+2 -1
View File
@@ -17,7 +17,7 @@ function isBailed(value: any) {
}
export type Disposable = () => void
export type VoidReturn = Promise<void> | void
export type VoidReturn = Promise<any> | any
export interface EventMap {
'app/started': () => void
@@ -57,6 +57,7 @@ export interface EventMap {
'problem/list': (query: FilterQuery<Pdoc>, handler: any) => VoidReturn
'problem/setting': (update: Partial<Pdoc>, handler: any) => VoidReturn
'problem/get': (doc: Pdoc, handler: any) => VoidReturn
'problem/delete': (domainId: string, docId: number) => VoidReturn
'training/list': (query: FilterQuery<TrainingDoc>, handler: any) => VoidReturn
'training/get': (tdoc: TrainingDoc, handler: any) => VoidReturn
+67 -130
View File
@@ -293,13 +293,68 @@ export async function prepare() {
}));
}
export class Handler {
UIContext: any;
args: any;
export class HandlerCommon {
domainId: string;
domain: DomainDoc;
user: User;
session: Record<string, any>;
request: Record<string, any>;
async limitRate(op: string, periodSecs: number, maxOperations: number) {
await opcount.inc(op, this.request.ip, periodSecs, maxOperations);
}
translate(str: string) {
if (!str) return '';
return str.toString().translate(this.user.viewLang, this.session.viewLang);
}
renderTitle(str: string) {
return `${this.translate(str)} - ${system.get('server.name')}`;
}
checkPerm(...args: bigint[]) {
// @ts-ignore
if (!this.user.hasPerm(...args)) throw new PermissionError(...args);
}
checkPriv(...args: number[]) {
// @ts-ignore
if (!this.user.hasPriv(...args)) throw new PrivilegeError(...args);
}
url(name: string, kwargs: any = {}) {
let res = '#';
const args: any = {};
const query: any = {};
for (const key in kwargs) {
if (kwargs[key] instanceof ObjectID) args[key] = kwargs[key].toHexString();
else args[key] = kwargs[key].toString();
}
for (const key in kwargs.query || {}) {
if (query[key] instanceof ObjectID) query[key] = kwargs.query[key].toHexString();
else query[key] = kwargs.query[key].toString();
}
try {
if (this.domainId !== 'system' || args.domainId) {
name += '_with_domainId';
args.domainId = args.domainId || this.domainId;
}
const { anchor } = args;
res = router.url(name, args, { query });
if (anchor) return `${res}#${anchor}`;
} catch (e) {
logger.warn(e.message);
logger.info('%s %o', name, args);
logger.info('%s', e.stack);
}
return res;
}
}
export class Handler extends HandlerCommon {
UIContext: any;
args: any;
ctx: Koa.Context;
request: {
@@ -327,19 +382,12 @@ export class Handler {
attachment: (name: string, stream?: any) => void,
};
session: any;
csrfToken: string;
user: User;
domain: DomainDoc;
loginMethods: any;
__param: Record<string, ParamOption[]>;
constructor(ctx: Koa.Context) {
super();
this.ctx = ctx;
this.request = {
host: ctx.request.host,
@@ -405,57 +453,6 @@ export class Handler {
return res;
}
async limitRate(op: string, periodSecs: number, maxOperations: number) {
await opcount.inc(op, this.request.ip, periodSecs, maxOperations);
}
translate(str: string) {
if (!str) return '';
return str.toString().translate(this.user.viewLang, this.session.viewLang);
}
renderTitle(str: string) {
return `${this.translate(str)} - ${system.get('server.name')}`;
}
checkPerm(...args: bigint[]) {
// @ts-ignore
if (!this.user.hasPerm(...args)) throw new PermissionError(...args);
}
checkPriv(...args: number[]) {
// @ts-ignore
if (!this.user.hasPriv(...args)) throw new PrivilegeError(...args);
}
url(name: string, kwargs: any = {}) {
let res = '#';
const args: any = {};
const query: any = {};
for (const key in kwargs) {
if (kwargs[key] instanceof ObjectID) args[key] = kwargs[key].toHexString();
else args[key] = kwargs[key].toString();
}
for (const key in kwargs.query || {}) {
if (query[key] instanceof ObjectID) query[key] = kwargs.query[key].toHexString();
else query[key] = kwargs.query[key].toString();
}
try {
if (this.domainId !== 'system' || args.domainId) {
name += '_with_domainId';
args.domainId = args.domainId || this.domainId;
}
const { anchor } = args;
res = router.url(name, args, { query });
if (anchor) return `${res}#${anchor}`;
} catch (e) {
logger.warn(e.message);
logger.info('%s %o', name, args);
logger.info('%s', e.stack);
}
return res;
}
async render(name: string, context: any) {
this.response.body = await this.renderHTML(name, context);
this.response.type = 'text/html';
@@ -584,8 +581,8 @@ export class Handler {
async saveCookie() {
const expireSeconds = this.session.save
? system.get('session.expire_seconds') as number
: system.get('session.unsaved_expire_seconds') as number;
? system.get('session.expire_seconds')
: system.get('session.unsaved_expire_seconds');
if (this.session._id) {
await token.update(
this.session._id,
@@ -614,10 +611,6 @@ export class Handler {
secure: !!system.get('session.secure'),
httpOnly: false,
};
if (this.session.save) {
cookie.expires = this.session.expireAt;
cookie.maxAge = expireSeconds;
}
this.ctx.cookies.set('sid', this.session._id, cookie);
}
@@ -717,26 +710,17 @@ export function Route(name: string, path: string, RouteHandler: any, ...permPriv
router.all(`${name}_with_domainId`, `/d/:domainId${path}`, (ctx) => handle(ctx, RouteHandler, checker));
}
export class ConnectionHandler {
export class ConnectionHandler extends HandlerCommon {
conn: sockjs.Connection;
args: any;
request: {
params: any;
headers: any;
ip: string;
}
domainId: string;
session: any;
args: any;
user: User;
domain: DomainDoc;
constructor(conn: sockjs.Connection) {
super();
this.conn = conn;
this.request = {
params: {},
@@ -758,54 +742,6 @@ export class ConnectionHandler {
return res;
}
async limitRate(op: string, periodSecs: number, maxOperations: number) {
await opcount.inc(op, this.request.ip, periodSecs, maxOperations);
}
translate(str: string) {
return str ? str.toString().translate(this.user.viewLang || this.session.viewLang) : '';
}
renderTitle(str: string) {
return `${this.translate(str)} - ${system.get('server.name')}`;
}
checkPerm(...args: bigint[]) {
if (!this.user.hasPerm(...args)) throw new PermissionError(...args);
}
checkPriv(...args: number[]) {
// @ts-ignore
if (!this.user.hasPriv(...args)) throw new PrivilegeError(...args);
}
url(name: string, kwargs: any = {}) {
let res = '#';
const args: any = {};
const query: any = {};
for (const key in kwargs) {
if (kwargs[key] instanceof ObjectID) args[key] = kwargs[key].toHexString();
else args[key] = kwargs[key].toString();
}
for (const key in kwargs.query || {}) {
if (query[key] instanceof ObjectID) query[key] = kwargs.query[key].toHexString();
else query[key] = kwargs.query[key].toString();
}
try {
if (this.args.domainId !== 'system' || args.domainId) {
name += '_with_domainId';
args.domainId = args.domainId || this.args.domainId;
}
const { anchor } = args;
res = router.url(name, args, { query });
if (anchor) return `${res}#${anchor}`;
} catch (e) {
logger.error(e.message);
logger.info(name, args);
}
return res;
}
send(data: any) {
this.conn.write(JSON.stringify(data));
}
@@ -934,6 +870,7 @@ global.Hydro.service.server = {
route,
param,
requireCsrfToken,
HandlerCommon,
Handler,
ConnectionHandler,
Route,
+13
View File
@@ -4,6 +4,9 @@ import cluster from 'cluster';
import path from 'path';
import serialize from 'serialize-javascript';
import Lru from 'lru-cache';
import { ObjectID } from 'mongodb';
import { isMoment } from 'moment';
import { Moment } from 'moment-timezone';
declare global {
interface StringConstructor {
@@ -240,4 +243,14 @@ export namespace Time {
}
return `${ms}ms`;
}
export function getObjectID(timestamp: string | Date | Moment) {
let _timestamp: number;
if (typeof timestamp === 'string') _timestamp = new Date(timestamp).getTime();
else if (isMoment(timestamp)) _timestamp = timestamp.toDate().getTime();
else _timestamp = timestamp.getTime();
const hexSeconds = Math.floor(_timestamp / 1000).toString(16);
const constructedObjectId = new ObjectID(`${hexSeconds}0000000000000000`);
return constructedObjectId;
}
}