Menci
6 years ago
committed by
GitHub
60 changed files with 2822 additions and 2726 deletions
@ -1,8 +0,0 @@
|
||||
const { highlight } = require('syzoj-renderer'); |
||||
const objectHash = require('object-hash'); |
||||
|
||||
module.exports = async (code, lang, cb) => { |
||||
highlight(code, lang, syzoj.redisCache, { |
||||
wrapper: null |
||||
}).then(cb); |
||||
} |
@ -1,33 +0,0 @@
|
||||
const { markdown } = require('syzoj-renderer'); |
||||
const XSS = require('xss'); |
||||
const CSSFilter = require('cssfilter'); |
||||
const xssWhiteList = Object.assign({}, require('xss/lib/default').whiteList); |
||||
|
||||
delete xssWhiteList.audio; |
||||
delete xssWhiteList.video; |
||||
|
||||
for (const tag in xssWhiteList) { |
||||
xssWhiteList[tag] = xssWhiteList[tag].concat(['style', 'class']); |
||||
} |
||||
|
||||
const xss = new XSS.FilterXSS({ |
||||
whiteList: xssWhiteList, |
||||
stripIgnoreTag: true, |
||||
onTagAttr: (tag, name, value, isWhiteAttr) => { |
||||
if (tag.toLowerCase() === 'img' && name.toLowerCase() === 'src' && value.startsWith('data:image/')) { |
||||
return name + '="' + XSS.escapeAttrValue(value) + '"'; |
||||
} |
||||
} |
||||
}); |
||||
|
||||
function filter(html) { |
||||
html = xss.process(html); |
||||
if (html) { |
||||
html = `<div style="position: relative; overflow: hidden; ">${html}</div>`; |
||||
} |
||||
return html; |
||||
}; |
||||
|
||||
module.exports = (markdownCode, callback) => { |
||||
markdown(markdownCode, syzoj.redisCache, filter).then(callback); |
||||
}; |
@ -0,0 +1,32 @@
|
||||
const child_process = require('child_process'); |
||||
|
||||
const rendererd = child_process.fork(__dirname + '/rendererd', [syzoj.config.redis]); |
||||
|
||||
const resolver = {}; |
||||
let currentId = 0; |
||||
|
||||
rendererd.on('message', msg => { |
||||
resolver[msg.id](msg.result); |
||||
delete resolver[msg.id]; |
||||
}); |
||||
|
||||
exports.markdown = (markdownCode, callback) => { |
||||
resolver[++currentId] = callback; |
||||
rendererd.send({ |
||||
id: currentId, |
||||
type: 'markdown', |
||||
source: markdownCode |
||||
}); |
||||
} |
||||
|
||||
exports.highlight = (code, lang, callback) => { |
||||
resolver[++currentId] = callback; |
||||
rendererd.send({ |
||||
id: currentId, |
||||
type: 'highlight', |
||||
source: { |
||||
code, |
||||
lang |
||||
} |
||||
}); |
||||
} |
@ -0,0 +1,61 @@
|
||||
const renderer = require('syzoj-renderer'); |
||||
const XSS = require('xss'); |
||||
const xssWhiteList = Object.assign({}, require('xss/lib/default').whiteList); |
||||
delete xssWhiteList.audio; |
||||
delete xssWhiteList.video; |
||||
|
||||
for (const tag in xssWhiteList) { |
||||
xssWhiteList[tag] = xssWhiteList[tag].concat(['style', 'class']); |
||||
} |
||||
|
||||
const xss = new XSS.FilterXSS({ |
||||
whiteList: xssWhiteList, |
||||
stripIgnoreTag: true, |
||||
onTagAttr: (tag, name, value, isWhiteAttr) => { |
||||
if (tag.toLowerCase() === 'img' && name.toLowerCase() === 'src' && value.startsWith('data:image/')) { |
||||
return name + '="' + XSS.escapeAttrValue(value) + '"'; |
||||
} |
||||
} |
||||
}); |
||||
|
||||
const Redis = require('redis'); |
||||
const util = require('util'); |
||||
const redis = Redis.createClient(process.argv[2]); |
||||
const redisCache = { |
||||
get: util.promisify(redis.get).bind(redis), |
||||
set: util.promisify(redis.set).bind(redis) |
||||
}; |
||||
|
||||
async function highlight(code, lang) { |
||||
return await renderer.highlight(code, lang, redisCache, { |
||||
wrapper: null |
||||
}); |
||||
} |
||||
|
||||
async function markdown(markdownCode) { |
||||
function filter(html) { |
||||
html = xss.process(html); |
||||
if (html) { |
||||
html = `<div style="position: relative; overflow: hidden; ">${html}</div>`; |
||||
} |
||||
return html; |
||||
}; |
||||
|
||||
return await renderer.markdown(markdownCode, redisCache, filter); |
||||
} |
||||
|
||||
process.on('message', async msg => { |
||||
if (msg.type === 'markdown') { |
||||
process.send({ |
||||
id: msg.id, |
||||
result: await markdown(msg.source) |
||||
}); |
||||
} else if (msg.type === 'highlight') { |
||||
process.send({ |
||||
id: msg.id, |
||||
result: await highlight(msg.source.code, msg.source.lang) |
||||
}); |
||||
} |
||||
}); |
||||
|
||||
process.on('disconnect', () => process.exit()); |
@ -0,0 +1,37 @@
|
||||
/* |
||||
* This script will help you build submission_statistics table. SYZOJ changed previous |
||||
* way of querying every time to cache statistics in database and update it for every |
||||
* judged submission. Without running this script after migrating will cause old submissions |
||||
* disappear from statistics. |
||||
*
|
||||
*/ |
||||
|
||||
const fn = async () => { |
||||
require('..'); |
||||
await syzoj.untilStarted; |
||||
|
||||
const User = syzoj.model('user'); |
||||
const Problem = syzoj.model('problem'); |
||||
const JudgeState = syzoj.model('judge_state'); |
||||
|
||||
const userIDs = (await User.createQueryBuilder().select('id').getRawMany()).map(record => record.id); |
||||
for (const id of userIDs) { |
||||
const problemIDs = (await JudgeState.createQueryBuilder() |
||||
.select('DISTINCT(problem_id)', 'problem_id') |
||||
.where('status = :status', { status: 'Accepted' }) |
||||
.andWhere('user_id = :user_id', { user_id: id }) |
||||
.andWhere('type = 0') |
||||
.getRawMany()).map(record => record.problem_id); |
||||
for (const problemID of problemIDs) { |
||||
const problem = await Problem.findById(problemID); |
||||
await problem.updateStatistics(id); |
||||
|
||||
console.log(`userID = ${id}, problemID = ${problemID}`); |
||||
} |
||||
} |
||||
|
||||
process.exit(); |
||||
}; |
||||
|
||||
// NOTE: Uncomment to run.
|
||||
fn(); |
@ -1,56 +0,0 @@
|
||||
let Sequelize = require('sequelize'); |
||||
let db = syzoj.db; |
||||
|
||||
let User = syzoj.model('user'); |
||||
let Article = syzoj.model('article'); |
||||
|
||||
let model = db.define('comment', { |
||||
id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, |
||||
|
||||
content: { type: Sequelize.TEXT }, |
||||
|
||||
article_id: { type: Sequelize.INTEGER }, |
||||
|
||||
user_id: { type: Sequelize.INTEGER }, |
||||
|
||||
public_time: { type: Sequelize.INTEGER } |
||||
}, { |
||||
timestamps: false, |
||||
tableName: 'comment', |
||||
indexes: [ |
||||
{ |
||||
fields: ['article_id'] |
||||
}, |
||||
{ |
||||
fields: ['user_id'] |
||||
} |
||||
] |
||||
}); |
||||
|
||||
let Model = require('./common'); |
||||
class ArticleComment extends Model { |
||||
static async create(val) { |
||||
return ArticleComment.fromRecord(ArticleComment.model.build(Object.assign({ |
||||
content: '', |
||||
article_id: 0, |
||||
user_id: 0, |
||||
public_time: 0, |
||||
}, val))); |
||||
} |
||||
|
||||
async loadRelationships() { |
||||
this.user = await User.fromID(this.user_id); |
||||
this.article = await Article.fromID(this.article_id); |
||||
} |
||||
|
||||
async isAllowedEditBy(user) { |
||||
await this.loadRelationships(); |
||||
return user && (user.is_admin || this.user_id === user.id || user.id === this.article.user_id); |
||||
} |
||||
|
||||
getModel() { return model; } |
||||
}; |
||||
|
||||
ArticleComment.model = model; |
||||
|
||||
module.exports = ArticleComment; |
@ -0,0 +1,38 @@
|
||||
import * as TypeORM from "typeorm"; |
||||
import Model from "./common"; |
||||
|
||||
import User from "./user"; |
||||
import Article from "./article"; |
||||
|
||||
@TypeORM.Entity() |
||||
export default class ArticleComment extends Model { |
||||
@TypeORM.PrimaryGeneratedColumn() |
||||
id: number; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "text" }) |
||||
content: string; |
||||
|
||||
@TypeORM.Index() |
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
article_id: number; |
||||
|
||||
@TypeORM.Index() |
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
user_id: number; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
public_time: number; |
||||
|
||||
user?: User; |
||||
article?: Article; |
||||
|
||||
async loadRelationships() { |
||||
this.user = await User.findById(this.user_id); |
||||
this.article = await Article.findById(this.article_id); |
||||
} |
||||
|
||||
async isAllowedEditBy(user) { |
||||
await this.loadRelationships(); |
||||
return user && (user.is_admin || this.user_id === user.id || user.id === this.article.user_id); |
||||
} |
||||
}; |
@ -1,91 +0,0 @@
|
||||
let Sequelize = require('sequelize'); |
||||
let db = syzoj.db; |
||||
|
||||
let User = syzoj.model('user'); |
||||
const Problem = syzoj.model('problem'); |
||||
|
||||
let model = db.define('article', { |
||||
id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, |
||||
|
||||
title: { type: Sequelize.STRING(80) }, |
||||
content: { type: Sequelize.TEXT('medium') }, |
||||
|
||||
user_id: { type: Sequelize.INTEGER }, |
||||
problem_id: { type: Sequelize.INTEGER }, |
||||
|
||||
public_time: { type: Sequelize.INTEGER }, |
||||
update_time: { type: Sequelize.INTEGER }, |
||||
sort_time: { type: Sequelize.INTEGER }, |
||||
|
||||
comments_num: { type: Sequelize.INTEGER }, |
||||
allow_comment: { type: Sequelize.BOOLEAN }, |
||||
|
||||
is_notice: { type: Sequelize.BOOLEAN } |
||||
}, { |
||||
timestamps: false, |
||||
tableName: 'article', |
||||
indexes: [ |
||||
{ |
||||
fields: ['user_id'] |
||||
}, |
||||
{ |
||||
fields: ['problem_id'] |
||||
}, |
||||
{ |
||||
fields: ['sort_time'] |
||||
} |
||||
] |
||||
}); |
||||
|
||||
let Model = require('./common'); |
||||
class Article extends Model { |
||||
static async create(val) { |
||||
return Article.fromRecord(Article.model.build(Object.assign({ |
||||
title: '', |
||||
content: '', |
||||
|
||||
user_id: 0, |
||||
problem_id: 0, |
||||
|
||||
public_time: 0, |
||||
update_time: 0, |
||||
sort_time: 0, |
||||
|
||||
comments_num: 0, |
||||
allow_comment: true, |
||||
|
||||
is_notice: false |
||||
}, val))); |
||||
} |
||||
|
||||
async loadRelationships() { |
||||
this.user = await User.fromID(this.user_id); |
||||
} |
||||
|
||||
async isAllowedEditBy(user) { |
||||
return user && (user.is_admin || this.user_id === user.id); |
||||
} |
||||
|
||||
async isAllowedCommentBy(user) { |
||||
return user && (this.allow_comment || user.is_admin || this.user_id === user.id); |
||||
} |
||||
|
||||
async resetReplyCountAndTime() { |
||||
let ArticleComment = syzoj.model('article-comment'); |
||||
await syzoj.utils.lock(['Article::resetReplyCountAndTime', this.id], async () => { |
||||
this.comments_num = await ArticleComment.count({ article_id: this.id }); |
||||
if (this.comments_num === 0) { |
||||
this.sort_time = this.public_time; |
||||
} else { |
||||
this.sort_time = await ArticleComment.model.max('public_time', { where: { article_id: this.id } }); |
||||
} |
||||
await this.save(); |
||||
}); |
||||
} |
||||
|
||||
getModel() { return model; } |
||||
}; |
||||
|
||||
Article.model = model; |
||||
|
||||
module.exports = Article; |
@ -0,0 +1,80 @@
|
||||
import * as TypeORM from "typeorm"; |
||||
import Model from "./common"; |
||||
|
||||
import User from "./user"; |
||||
import Problem from "./problem"; |
||||
import ArticleComment from "./article-comment"; |
||||
|
||||
declare var syzoj: any; |
||||
|
||||
@TypeORM.Entity() |
||||
export default class Article extends Model { |
||||
static cache = true; |
||||
|
||||
@TypeORM.PrimaryGeneratedColumn() |
||||
id: number; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "varchar", length: 80 }) |
||||
title: string; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "mediumtext" }) |
||||
content: string; |
||||
|
||||
@TypeORM.Index() |
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
user_id: number; |
||||
|
||||
@TypeORM.Index() |
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
problem_id: number; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
public_time: number; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
update_time: number; |
||||
|
||||
@TypeORM.Index() |
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
sort_time: number; |
||||
|
||||
@TypeORM.Column({ default: 0, type: "integer" }) |
||||
comments_num: number; |
||||
|
||||
@TypeORM.Column({ default: true, type: "boolean" }) |
||||
allow_comment: boolean; |
||||
|
||||
@TypeORM.Index() |
||||
@TypeORM.Column({ nullable: true, type: "boolean" }) |
||||
is_notice: boolean; |
||||
|
||||
user?: User; |
||||
problem?: Problem; |
||||
|
||||
async loadRelationships() { |
||||
this.user = await User.findById(this.user_id); |
||||
} |
||||
|
||||
async isAllowedEditBy(user) { |
||||
return user && (user.is_admin || this.user_id === user.id); |
||||
} |
||||
|
||||
async isAllowedCommentBy(user) { |
||||
return user && (this.allow_comment || user.is_admin || this.user_id === user.id); |
||||
} |
||||
|
||||
async resetReplyCountAndTime() { |
||||
await syzoj.utils.lock(['Article::resetReplyCountAndTime', this.id], async () => { |
||||
this.comments_num = await ArticleComment.count({ article_id: this.id }); |
||||
if (this.comments_num === 0) { |
||||
this.sort_time = this.public_time; |
||||
} else { |
||||
this.sort_time = (await ArticleComment.findOne({ |
||||
where: { article_id: this.id }, |
||||
order: { public_time: "DESC" } |
||||
})).public_time; |
||||
} |
||||
await this.save(); |
||||
}); |
||||
} |
||||
}; |
@ -1,136 +0,0 @@
|
||||
let Sequelize = require('sequelize'); |
||||
|
||||
class Model { |
||||
constructor(record) { |
||||
this.record = record; |
||||
this.loadFields(); |
||||
} |
||||
|
||||
loadFields() { |
||||
let model = this.getModel(); |
||||
let obj = JSON.parse(JSON.stringify(this.record.get({ plain: true }))); |
||||
for (let key in obj) { |
||||
if (!model.tableAttributes[key]) continue; |
||||
|
||||
if (model.tableAttributes[key].type instanceof Sequelize.JSON && typeof obj[key] === 'string') { |
||||
try { |
||||
this[key] = JSON.parse(obj[key]); |
||||
} catch (e) { |
||||
this[key] = {}; |
||||
} |
||||
} else this[key] = obj[key]; |
||||
} |
||||
} |
||||
|
||||
toPlain() { |
||||
let model = this.getModel(); |
||||
let obj = JSON.parse(JSON.stringify(this.record.get({ plain: true }))); |
||||
for (let key in obj) { |
||||
obj[key] = this[key]; |
||||
} |
||||
return obj; |
||||
} |
||||
|
||||
async save() { |
||||
let obj = this.toPlain(); |
||||
for (let key in obj) this.record.set(key, obj[key]); |
||||
|
||||
let isNew = this.record.isNewRecord; |
||||
|
||||
await syzoj.utils.withTimeoutRetry(() => this.record.save()); |
||||
if (!isNew) return; |
||||
|
||||
await this.reload(); |
||||
} |
||||
|
||||
async reload() { |
||||
await this.record.reload(); |
||||
this.loadFields(); |
||||
} |
||||
|
||||
async destroy() { |
||||
return this.record.destroy(); |
||||
} |
||||
|
||||
static async fromRecord(record) { |
||||
record = await record; |
||||
if (!record) return null; |
||||
return new this(await record); |
||||
} |
||||
|
||||
static async fromID(id) { |
||||
return this.fromRecord(this.model.findByPk(id)); |
||||
} |
||||
|
||||
static async findOne(options) { |
||||
return this.fromRecord(this.model.findOne(options)); |
||||
} |
||||
|
||||
static async all() { |
||||
return (await this.model.findAll()).mapAsync(record => (this.fromRecord(record))); |
||||
} |
||||
|
||||
static async count(where) { |
||||
// count(sql)
|
||||
if (typeof where === 'string') { |
||||
let sql = where; |
||||
return syzoj.db.countQuery(sql); |
||||
} |
||||
|
||||
// count(where)
|
||||
return this.model.count({ where: where }); |
||||
} |
||||
|
||||
static async query(paginate, where, order, largeData) { |
||||
let records = []; |
||||
|
||||
if (typeof paginate === 'string') { |
||||
// query(sql)
|
||||
let sql = paginate; |
||||
records = await syzoj.db.query(sql, { model: this.model }); |
||||
} else { |
||||
if (paginate && !Array.isArray(paginate) && !paginate.pageCnt) return []; |
||||
|
||||
let options = { |
||||
where: where, |
||||
order: order |
||||
}; |
||||
if (Array.isArray(paginate)) { |
||||
options.offset = paginate[0] - 1; |
||||
options.limit = paginate[1] - paginate[0] + 1; |
||||
} else if (paginate) { |
||||
options.offset = (paginate.currPage - 1) * paginate.perPage; |
||||
options.limit = parseInt(paginate.perPage); |
||||
} |
||||
|
||||
if (!largeData) records = await this.model.findAll(options); |
||||
else { |
||||
let sql = await getSqlFromFindAll(this.model, options); |
||||
let i = sql.indexOf('FROM'); |
||||
sql = 'SELECT id ' + sql.substr(i, sql.length - i); |
||||
sql = `SELECT a.* FROM (${sql}) AS b JOIN ${this.model.name} AS a ON a.id = b.id ORDER BY id DESC`; |
||||
records = await syzoj.db.query(sql, { model: this.model }); |
||||
} |
||||
} |
||||
|
||||
return records.mapAsync(record => (this.fromRecord(record))); |
||||
} |
||||
} |
||||
|
||||
function getSqlFromFindAll(Model, options) { |
||||
let id = require('uuid')(); |
||||
|
||||
return new Promise((resolve, reject) => { |
||||
Model.addHook('beforeFindAfterOptions', id, options => { |
||||
Model.removeHook('beforeFindAfterOptions', id); |
||||
|
||||
resolve(Model.sequelize.dialect.QueryGenerator.selectQuery(Model.getTableName(), options, Model).slice(0, -1)); |
||||
|
||||
return new Promise(() => {}); |
||||
}); |
||||
|
||||
return Model.findAll(options).catch(reject); |
||||
}); |
||||
} |
||||
|
||||
module.exports = Model; |
@ -0,0 +1,208 @@
|
||||
import * as TypeORM from "typeorm"; |
||||
import * as LRUCache from "lru-cache"; |
||||
import * as DeepCopy from "deepcopy"; |
||||
|
||||
declare var syzoj: any; |
||||
|
||||
interface Paginater { |
||||
pageCnt: number; |
||||
perPage: number; |
||||
currPage: number; |
||||
} |
||||
|
||||
enum PaginationType { |
||||
PREV = -1, |
||||
NEXT = 1 |
||||
} |
||||
|
||||
enum PaginationIDOrder { |
||||
ASC = 1, |
||||
DESC = -1 |
||||
} |
||||
|
||||
const caches: Map<string, LRUCache<number, Model>> = new Map(); |
||||
|
||||
function ensureCache(modelName) { |
||||
if (!caches.has(modelName)) { |
||||
caches.set(modelName, new LRUCache({ |
||||
max: syzoj.config.db.cache_size |
||||
})); |
||||
} |
||||
|
||||
return caches.get(modelName); |
||||
} |
||||
|
||||
function cacheGet(modelName, id) { |
||||
return ensureCache(modelName).get(parseInt(id)); |
||||
} |
||||
|
||||
function cacheSet(modelName, id, data) { |
||||
if (data == null) { |
||||
ensureCache(modelName).del(id); |
||||
} else { |
||||
ensureCache(modelName).set(parseInt(id), data); |
||||
} |
||||
} |
||||
|
||||
export default class Model extends TypeORM.BaseEntity { |
||||
static cache = false; |
||||
|
||||
static async findById<T extends TypeORM.BaseEntity>(this: TypeORM.ObjectType<T>, id?: number): Promise<T | undefined> { |
||||
const doQuery = async () => await (this as any).findOne(parseInt(id as any) || 0); |
||||
|
||||
if ((this as typeof Model).cache) { |
||||
const resultObject = cacheGet(this.name, id); |
||||
if (resultObject) { |
||||
return (this as typeof Model).create(resultObject) as any as T; |
||||
} |
||||
|
||||
const result = await doQuery(); |
||||
if (result) { |
||||
cacheSet(this.name, id, result.toPlain()); |
||||
} |
||||
return result; |
||||
} else { |
||||
return await doQuery(); |
||||
} |
||||
} |
||||
|
||||
toPlain() { |
||||
const object = {}; |
||||
TypeORM.getConnection().getMetadata(this.constructor).ownColumns.map(column => column.propertyName).forEach(key => { |
||||
object[key] = DeepCopy(this[key]); |
||||
}); |
||||
return object; |
||||
} |
||||
|
||||
async destroy() { |
||||
const id = (this as any).id; |
||||
await TypeORM.getManager().remove(this); |
||||
await (this.constructor as typeof Model).deleteFromCache(id); |
||||
} |
||||
|
||||
static async deleteFromCache(id) { |
||||
if (this.cache) { |
||||
cacheSet(this.name, id, null); |
||||
} |
||||
} |
||||
|
||||
async save(): Promise<this> { |
||||
await super.save(); |
||||
if ((this.constructor as typeof Model).cache) { |
||||
cacheSet(this.constructor.name, (this as any).id, this); |
||||
} |
||||
return this; |
||||
} |
||||
|
||||
static async countQuery<T extends TypeORM.BaseEntity>(this: TypeORM.ObjectType<T>, query: TypeORM.SelectQueryBuilder<T> | string) { |
||||
let parameters: any[] = null; |
||||
if (typeof query !== 'string') { |
||||
[query, parameters] = query.getQueryAndParameters(); |
||||
} |
||||
|
||||
return parseInt(( |
||||
await TypeORM.getManager().query(`SELECT COUNT(*) FROM (${query}) AS \`__tmp_table\``, parameters) |
||||
)[0]['COUNT(*)']); |
||||
} |
||||
|
||||
static async countForPagination(where) { |
||||
const queryBuilder = where instanceof TypeORM.SelectQueryBuilder |
||||
? where |
||||
: this.createQueryBuilder().where(where); |
||||
return await queryBuilder.getCount(); |
||||
} |
||||
|
||||
static async queryAll(queryBuilder) { |
||||
return await queryBuilder.getMany(); |
||||
} |
||||
|
||||
static async queryPage(paginater: Paginater, where, order, largeData = false) { |
||||
if (!paginater.pageCnt) return []; |
||||
|
||||
const queryBuilder = where instanceof TypeORM.SelectQueryBuilder |
||||
? where |
||||
: this.createQueryBuilder().where(where); |
||||
|
||||
if (order) queryBuilder.orderBy(order); |
||||
|
||||
queryBuilder.skip((paginater.currPage - 1) * paginater.perPage) |
||||
.take(paginater.perPage); |
||||
|
||||
if (largeData) { |
||||
const rawResult = await queryBuilder.select('id').getRawMany(); |
||||
return await Promise.all(rawResult.map(async result => this.findById(result.id))); |
||||
} |
||||
|
||||
return queryBuilder.getMany(); |
||||
} |
||||
|
||||
static async queryPageFast<T extends TypeORM.BaseEntity>(this: TypeORM.ObjectType<T>, |
||||
queryBuilder: TypeORM.SelectQueryBuilder<T>, |
||||
{ currPageTop, currPageBottom, perPage }, |
||||
idOrder: PaginationIDOrder, |
||||
pageType: PaginationType) { |
||||
const queryBuilderBak = queryBuilder.clone(); |
||||
|
||||
const result = { |
||||
meta: { |
||||
hasPrevPage: false, |
||||
hasNextPage: false, |
||||
top: 0, |
||||
bottom: 0 |
||||
}, |
||||
data: [] |
||||
}; |
||||
|
||||
queryBuilder.take(perPage); |
||||
if (pageType === PaginationType.PREV) { |
||||
if (currPageTop != null) { |
||||
queryBuilder.andWhere(`id ${idOrder === PaginationIDOrder.DESC ? '>' : '<'} :currPageTop`, { currPageTop }); |
||||
queryBuilder.orderBy('id', idOrder === PaginationIDOrder.DESC ? 'ASC' : 'DESC'); |
||||
} |
||||
} else if (pageType === PaginationType.NEXT) { |
||||
if (currPageBottom != null) { |
||||
queryBuilder.andWhere(`id ${idOrder === PaginationIDOrder.DESC ? '<' : '>'} :currPageBottom`, { currPageBottom }); |
||||
queryBuilder.orderBy('id', idOrder === PaginationIDOrder.DESC ? 'DESC' : 'ASC'); |
||||
} |
||||
} else queryBuilder.orderBy('id', idOrder === PaginationIDOrder.DESC ? 'DESC' : 'ASC'); |
||||
|
||||
result.data = await queryBuilder.getMany(); |
||||
result.data.sort((a, b) => (a.id - b.id) * idOrder); |
||||
|
||||
if (result.data.length === 0) return result; |
||||
|
||||
const queryBuilderHasPrev = queryBuilderBak.clone(), |
||||
queryBuilderHasNext = queryBuilderBak; |
||||
|
||||
result.meta.top = result.data[0].id; |
||||
result.meta.bottom = result.data[result.data.length - 1].id; |
||||
|
||||
// Run two queries in parallel.
|
||||
await Promise.all(([ |
||||
async () => result.meta.hasPrevPage = !!(await queryBuilderHasPrev.andWhere(`id ${idOrder === PaginationIDOrder.DESC ? '>' : '<'} :id`, { |
||||
id: result.meta.top |
||||
}).take(1).getOne()), |
||||
async () => result.meta.hasNextPage = !!(await queryBuilderHasNext.andWhere(`id ${idOrder === PaginationIDOrder.DESC ? '<' : '>'} :id`, { |
||||
id: result.meta.bottom |
||||
}).take(1).getOne()) |
||||
]).map(f => f())); |
||||
|
||||
return result; |
||||
} |
||||
|
||||
static async queryRange(range: any[], where, order) { |
||||
range[0] = parseInt(range[0]); |
||||
range[1] = parseInt(range[1]); |
||||
|
||||
const queryBuilder = where instanceof TypeORM.SelectQueryBuilder |
||||
? where |
||||
: this.createQueryBuilder().where(where); |
||||
|
||||
if (order) queryBuilder.orderBy(order); |
||||
|
||||
queryBuilder.skip(range[0] - 1) |
||||
.take(range[1] - range[0] + 1); |
||||
|
||||
return queryBuilder.getMany(); |
||||
} |
||||
} |
@ -1,81 +0,0 @@
|
||||
let Sequelize = require('sequelize'); |
||||
let db = syzoj.db; |
||||
|
||||
let User = syzoj.model('user'); |
||||
let Problem = syzoj.model('problem'); |
||||
|
||||
let model = db.define('custom_test', { |
||||
id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, |
||||
|
||||
input_filepath: { type: Sequelize.TEXT }, |
||||
code: { type: Sequelize.TEXT('medium') }, |
||||
language: { type: Sequelize.STRING(20) }, |
||||
|
||||
status: { type: Sequelize.STRING(50) }, |
||||
|
||||
time: { type: Sequelize.INTEGER }, |
||||
pending: { type: Sequelize.BOOLEAN }, |
||||
memory: { type: Sequelize.INTEGER }, |
||||
|
||||
result: { type: Sequelize.JSON }, |
||||
|
||||
user_id: { type: Sequelize.INTEGER }, |
||||
|
||||
problem_id: { type: Sequelize.INTEGER }, |
||||
|
||||
submit_time: { type: Sequelize.INTEGER } |
||||
}, { |
||||
timestamps: false, |
||||
tableName: 'custom_test', |
||||
indexes: [ |
||||
{ |
||||
fields: ['status'], |
||||
}, |
||||
{ |
||||
fields: ['user_id'], |
||||
}, |
||||
{ |
||||
fields: ['problem_id'], |
||||
} |
||||
] |
||||
}); |
||||
|
||||
let Model = require('./common'); |
||||
class CustomTest extends Model { |
||||
static async create(val) { |
||||
return CustomTest.fromRecord(CustomTest.model.build(Object.assign({ |
||||
input_filepath: '', |
||||
code: '', |
||||
language: '', |
||||
user_id: 0, |
||||
problem_id: 0, |
||||
submit_time: parseInt((new Date()).getTime() / 1000), |
||||
|
||||
pending: true, |
||||
|
||||
time: 0, |
||||
memory: 0, |
||||
result: {}, |
||||
status: 'Waiting', |
||||
}, val))); |
||||
} |
||||
|
||||
async loadRelationships() { |
||||
this.user = await User.fromID(this.user_id); |
||||
this.problem = await Problem.fromID(this.problem_id); |
||||
} |
||||
|
||||
async updateResult(result) { |
||||
this.pending = result.pending; |
||||
this.status = result.status; |
||||
this.time = result.time_used; |
||||
this.memory = result.memory_used; |
||||
this.result = result; |
||||
} |
||||
|
||||
getModel() { return model; } |
||||
} |
||||
|
||||
CustomTest.model = model; |
||||
|
||||
module.exports = CustomTest; |
@ -1,31 +0,0 @@
|
||||
let Sequelize = require('sequelize'); |
||||
let db = syzoj.db; |
||||
|
||||
let model = db.define('formatted_code', { |
||||
key: { type: Sequelize.STRING(50), primaryKey: true }, |
||||
code: { type: Sequelize.TEXT('medium') } |
||||
}, { |
||||
timestamps: false, |
||||
tableName: 'formatted_code', |
||||
indexes: [ |
||||
{ |
||||
fields: ['key'] |
||||
} |
||||
] |
||||
}); |
||||
|
||||
let Model = require('./common'); |
||||
class FormattedCode extends Model { |
||||
static async create(val) { |
||||
return FormattedCode.fromRecord(FormattedCode.model.build(Object.assign({ |
||||
key: "", |
||||
code: "" |
||||
}, val))); |
||||
} |
||||
|
||||
getModel() { return model; } |
||||
} |
||||
|
||||
FormattedCode.model = model; |
||||
|
||||
module.exports = FormattedCode; |
@ -0,0 +1,11 @@
|
||||
import * as TypeORM from "typeorm"; |
||||
import Model from "./common"; |
||||
|
||||
@TypeORM.Entity() |
||||
export default class FormattedCode extends Model { |
||||
@TypeORM.PrimaryColumn({ type: "varchar", length: 50 }) |
||||
key: string; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "mediumtext" }) |
||||
code: string; |
||||
} |
@ -1,195 +0,0 @@
|
||||
let Sequelize = require('sequelize'); |
||||
const randomstring = require('randomstring'); |
||||
let db = syzoj.db; |
||||
|
||||
let User = syzoj.model('user'); |
||||
let Problem = syzoj.model('problem'); |
||||
let Contest = syzoj.model('contest'); |
||||
|
||||
let Judger = syzoj.lib('judger'); |
||||
|
||||
let model = db.define('judge_state', { |
||||
id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, |
||||
|
||||
// The data zip's md5 if it's a submit-answer problem
|
||||
code: { type: Sequelize.TEXT('medium') }, |
||||
language: { type: Sequelize.STRING(20) }, |
||||
|
||||
status: { type: Sequelize.STRING(50) }, |
||||
task_id: { type: Sequelize.STRING(50) }, |
||||
score: { type: Sequelize.INTEGER }, |
||||
total_time: { type: Sequelize.INTEGER }, |
||||
code_length: { type: Sequelize.INTEGER }, |
||||
pending: { type: Sequelize.BOOLEAN }, |
||||
max_memory: { type: Sequelize.INTEGER }, |
||||
|
||||
// For NOI contest
|
||||
compilation: { type: Sequelize.JSON }, |
||||
|
||||
result: { type: Sequelize.JSON }, |
||||
|
||||
user_id: { type: Sequelize.INTEGER }, |
||||
|
||||
problem_id: { type: Sequelize.INTEGER }, |
||||
|
||||
submit_time: { type: Sequelize.INTEGER }, |
||||
/* |
||||
* "type" indicate it's contest's submission(type = 1) or normal submission(type = 0) |
||||
* if it's contest's submission (type = 1), the type_info is contest_id |
||||
* use this way represent because it's easy to expand // Menci:这锅我不背,是 Chenyao 留下来的坑。
|
||||
*/ |
||||
type: { type: Sequelize.INTEGER }, |
||||
type_info: { type: Sequelize.INTEGER }, |
||||
is_public: { type: Sequelize.BOOLEAN } |
||||
}, { |
||||
timestamps: false, |
||||
tableName: 'judge_state', |
||||
indexes: [ |
||||
{ |
||||
fields: ['status'], |
||||
}, |
||||
{ |
||||
fields: ['score'], |
||||
}, |
||||
{ |
||||
fields: ['user_id'], |
||||
}, |
||||
{ |
||||
fields: ['problem_id'], |
||||
}, |
||||
{ |
||||
fields: ['task_id'], |
||||
}, |
||||
{ |
||||
fields: ['id', 'is_public', 'type_info', 'type'] |
||||
} |
||||
] |
||||
}); |
||||
|
||||
let Model = require('./common'); |
||||
class JudgeState extends Model { |
||||
static async create(val) { |
||||
return JudgeState.fromRecord(JudgeState.model.build(Object.assign({ |
||||
code: '', |
||||
code_length: 0, |
||||
language: null, |
||||
user_id: 0, |
||||
problem_id: 0, |
||||
submit_time: parseInt((new Date()).getTime() / 1000), |
||||
|
||||
type: 0, |
||||
type_info: 0, |
||||
|
||||
pending: false, |
||||
|
||||
score: null, |
||||
total_time: null, |
||||
max_memory: null, |
||||
status: 'Unknown', |
||||
compilation: {}, |
||||
result: {}, |
||||
task_id: randomstring.generate(10), |
||||
is_public: false |
||||
}, val))); |
||||
} |
||||
|
||||
async loadRelationships() { |
||||
if (!this.user) { |
||||
this.user = await User.fromID(this.user_id); |
||||
} |
||||
if (!this.problem) { |
||||
if (this.problem_id) this.problem = await Problem.fromID(this.problem_id); |
||||
} |
||||
} |
||||
|
||||
async isAllowedVisitBy(user) { |
||||
await this.loadRelationships(); |
||||
|
||||
if (user && user.id === this.problem.user_id) return true; |
||||
else if (this.type === 0) return this.problem.is_public || (user && (await user.hasPrivilege('manage_problem'))); |
||||
else if (this.type === 1) { |
||||
let contest = await Contest.fromID(this.type_info); |
||||
if (contest.isRunning()) { |
||||
return user && await contest.isSupervisior(user); |
||||
} else { |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
|
||||
async updateRelatedInfo(newSubmission) { |
||||
if (this.type === 0) { |
||||
await this.loadRelationships(); |
||||
|
||||
// No need to await them.
|
||||
this.user.refreshSubmitInfo(); |
||||
this.problem.resetSubmissionCount(); |
||||
} else if (this.type === 1) { |
||||
let contest = await Contest.fromID(this.type_info); |
||||
await contest.newSubmission(this); |
||||
} |
||||
} |
||||
|
||||
async rejudge() { |
||||
await syzoj.utils.lock(['JudgeState::rejudge', this.id], async () => { |
||||
await this.loadRelationships(); |
||||
|
||||
let oldStatus = this.status; |
||||
|
||||
this.status = 'Unknown'; |
||||
this.pending = false; |
||||
this.score = null; |
||||
if (this.language) { |
||||
// language is empty if it's a submit-answer problem
|
||||
this.total_time = null; |
||||
this.max_memory = null; |
||||
} |
||||
this.result = {}; |
||||
this.task_id = randomstring.generate(10); |
||||
await this.save(); |
||||
|
||||
/* |
||||
let WaitingJudge = syzoj.model('waiting_judge'); |
||||
let waiting_judge = await WaitingJudge.create({ |
||||
judge_id: this.id, |
||||
priority: 2, |
||||
type: 'submission' |
||||
}); |
||||
|
||||
await waiting_judge.save(); |
||||
*/ |
||||
|
||||
await this.problem.resetSubmissionCount(); |
||||
if (oldStatus === 'Accepted') { |
||||
await this.user.refreshSubmitInfo(); |
||||
await this.user.save(); |
||||
} |
||||
|
||||
if (this.type === 1) { |
||||
let contest = await Contest.fromID(this.type_info); |
||||
await contest.newSubmission(this); |
||||
} |
||||
|
||||
try { |
||||
await Judger.judge(this, this.problem, 1); |
||||
this.pending = true; |
||||
this.status = 'Waiting'; |
||||
await this.save(); |
||||
} catch (err) { |
||||
console.log("Error while connecting to judge frontend: " + err.toString()); |
||||
throw new ErrorMessage("无法开始评测。"); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
async getProblemType() { |
||||
await this.loadRelationships(); |
||||
return this.problem.type; |
||||
} |
||||
|
||||
getModel() { return model; } |
||||
} |
||||
|
||||
JudgeState.model = model; |
||||
|
||||
module.exports = JudgeState; |
@ -0,0 +1,187 @@
|
||||
import * as TypeORM from "typeorm"; |
||||
import Model from "./common"; |
||||
|
||||
declare var syzoj, ErrorMessage: any; |
||||
|
||||
import User from "./user"; |
||||
import Problem from "./problem"; |
||||
import Contest from "./contest"; |
||||
|
||||
const Judger = syzoj.lib('judger'); |
||||
|
||||
enum Status { |
||||
ACCEPTED = "Accepted", |
||||
COMPILE_ERROR = "Compile Error", |
||||
FILE_ERROR = "File Error", |
||||
INVALID_INTERACTION = "Invalid Interaction", |
||||
JUDGEMENT_FAILED = "Judgement Failed", |
||||
MEMORY_LIMIT_EXCEEDED = "Memory Limit Exceeded", |
||||
NO_TESTDATA = "No Testdata", |
||||
OUTPUT_LIMIT_EXCEEDED = "Output Limit Exceeded", |
||||
PARTIALLY_CORRECT = "Partially Correct", |
||||
RUNTIME_ERROR = "Runtime Error", |
||||
SYSTEM_ERROR = "System Error", |
||||
TIME_LIMIT_EXCEEDED = "Time Limit Exceeded", |
||||
UNKNOWN = "Unknown", |
||||
WRONG_ANSWER = "Wrong Answer", |
||||
WAITING = "Waiting" |
||||
} |
||||
|
||||
@TypeORM.Entity() |
||||
@TypeORM.Index(['type', 'type_info']) |
||||
@TypeORM.Index(['type', 'is_public', 'language', 'status', 'problem_id']) |
||||
@TypeORM.Index(['type', 'is_public', 'status', 'problem_id']) |
||||
@TypeORM.Index(['type', 'is_public', 'problem_id']) |
||||
@TypeORM.Index(['type', 'is_public', 'language', 'problem_id']) |
||||
@TypeORM.Index(['problem_id', 'type', 'pending', 'score']) |
||||
export default class JudgeState extends Model { |
||||
@TypeORM.PrimaryGeneratedColumn() |
||||
id: number; |
||||
|
||||
// The data zip's md5 if it's a submit-answer problem
|
||||
@TypeORM.Column({ nullable: true, type: "mediumtext" }) |
||||
code: string; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "varchar", length: 20 }) |
||||
language: string; |
||||
|
||||
@TypeORM.Index() |
||||
@TypeORM.Column({ nullable: true, type: "enum", enum: Status }) |
||||
status: Status; |
||||
|
||||
@TypeORM.Index() |
||||
@TypeORM.Column({ nullable: true, type: "varchar", length: 50 }) |
||||
task_id: string; |
||||
|
||||
@TypeORM.Index() |
||||
@TypeORM.Column({ nullable: true, type: "integer", default: 0 }) |
||||
score: number; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "integer", default: 0 }) |
||||
total_time: number; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "integer", default: 0 }) |
||||
code_length: number; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "boolean", default: 0 }) |
||||
pending: boolean; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "integer", default: 0 }) |
||||
max_memory: number; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "json" }) |
||||
compilation: any; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "json" }) |
||||
result: any; |
||||
|
||||
@TypeORM.Index() |
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
user_id: number; |
||||
|
||||
@TypeORM.Index() |
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
problem_id: number; |
||||
|
||||
@TypeORM.Index() |
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
submit_time: number; |
||||
|
||||
/* |
||||
* "type" indicate it's contest's submission(type = 1) or normal submission(type = 0) |
||||
* if it's contest's submission (type = 1), the type_info is contest_id |
||||
* use this way represent because it's easy to expand // Menci:这锅我不背,是 Chenyao 留下来的坑。
|
||||
*/ |
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
type: number; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
type_info: number; |
||||
|
||||
@TypeORM.Index() |
||||
@TypeORM.Column({ nullable: true, type: "boolean" }) |
||||
is_public: boolean; |
||||
|
||||
user?: User; |
||||
problem?: Problem; |
||||
|
||||
async loadRelationships() { |
||||
if (!this.user) { |
||||
this.user = await User.findById(this.user_id); |
||||
} |
||||
if (!this.problem) { |
||||
if (this.problem_id) this.problem = await Problem.findById(this.problem_id); |
||||
} |
||||
} |
||||
|
||||
async isAllowedVisitBy(user) { |
||||
await this.loadRelationships(); |
||||
|
||||
if (user && user.id === this.problem.user_id) return true; |
||||
else if (this.type === 0) return this.problem.is_public || (user && (await user.hasPrivilege('manage_problem'))); |
||||
else if (this.type === 1) { |
||||
let contest = await Contest.findById(this.type_info); |
||||
if (contest.isRunning()) { |
||||
return user && await contest.isSupervisior(user); |
||||
} else { |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
|
||||
async updateRelatedInfo(newSubmission) { |
||||
if (this.type === 0) { |
||||
await this.loadRelationships(); |
||||
|
||||
const promises = []; |
||||
promises.push(this.user.refreshSubmitInfo()); |
||||
promises.push(this.problem.resetSubmissionCount()); |
||||
|
||||
if (!newSubmission) { |
||||
promises.push(this.problem.updateStatistics(this.user_id)); |
||||
} |
||||
|
||||
await Promise.all(promises); |
||||
} else if (this.type === 1) { |
||||
let contest = await Contest.findById(this.type_info); |
||||
await contest.newSubmission(this); |
||||
} |
||||
} |
||||
|
||||
async rejudge() { |
||||
await syzoj.utils.lock(['JudgeState::rejudge', this.id], async () => { |
||||
await this.loadRelationships(); |
||||
|
||||
let oldStatus = this.status; |
||||
|
||||
this.status = Status.UNKNOWN; |
||||
this.pending = false; |
||||
this.score = null; |
||||
if (this.language) { |
||||
// language is empty if it's a submit-answer problem
|
||||
this.total_time = null; |
||||
this.max_memory = null; |
||||
} |
||||
this.result = {}; |
||||
this.task_id = require('randomstring').generate(10); |
||||
await this.save(); |
||||
|
||||
await this.updateRelatedInfo(false); |
||||
|
||||
try { |
||||
await Judger.judge(this, this.problem, 1); |
||||
this.pending = true; |
||||
this.status = Status.WAITING; |
||||
await this.save(); |
||||
} catch (err) { |
||||
console.log("Error while connecting to judge frontend: " + err.toString()); |
||||
throw new ErrorMessage("无法开始评测。"); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
async getProblemType() { |
||||
await this.loadRelationships(); |
||||
return this.problem.type; |
||||
} |
||||
} |
@ -1,683 +0,0 @@
|
||||
let statisticsStatements = { |
||||
fastest: |
||||
'\ |
||||
SELECT \ |
||||
DISTINCT(`user_id`) AS `user_id`, \ |
||||
( \ |
||||
SELECT \ |
||||
`id` \ |
||||
FROM `judge_state` `inner_table` \ |
||||
WHERE `problem_id` = `outer_table`.`problem_id` AND `user_id` = `outer_table`.`user_id` AND `status` = "Accepted" AND `type` = 0 \ |
||||
ORDER BY `total_time` ASC \ |
||||
LIMIT 1 \ |
||||
) AS `id`, \ |
||||
( \ |
||||
SELECT \ |
||||
`total_time` \ |
||||
FROM `judge_state` `inner_table` \ |
||||
WHERE `problem_id` = `outer_table`.`problem_id` AND `user_id` = `outer_table`.`user_id` AND `status` = "Accepted" AND `type` = 0 \ |
||||
ORDER BY `total_time` ASC \ |
||||
LIMIT 1 \ |
||||
) AS `total_time` \ |
||||
FROM `judge_state` `outer_table` \ |
||||
WHERE \ |
||||
`problem_id` = __PROBLEM_ID__ AND `status` = "Accepted" AND `type` = 0 \ |
||||
ORDER BY `total_time` ASC \ |
||||
', |
||||
slowest: |
||||
' \ |
||||
SELECT \ |
||||
DISTINCT(`user_id`) AS `user_id`, \ |
||||
( \ |
||||
SELECT \ |
||||
`id` \ |
||||
FROM `judge_state` `inner_table` \ |
||||
WHERE `problem_id` = `outer_table`.`problem_id` AND `user_id` = `outer_table`.`user_id` AND `status` = "Accepted" AND `type` = 0 \ |
||||
ORDER BY `total_time` DESC \ |
||||
LIMIT 1 \ |
||||
) AS `id`, \ |
||||
( \ |
||||
SELECT \ |
||||
`total_time` \ |
||||
FROM `judge_state` `inner_table` \ |
||||
WHERE `problem_id` = `outer_table`.`problem_id` AND `user_id` = `outer_table`.`user_id` AND `status` = "Accepted" AND `type` = 0 \ |
||||
ORDER BY `total_time` DESC \ |
||||
LIMIT 1 \ |
||||
) AS `total_time` \ |
||||
FROM `judge_state` `outer_table` \ |
||||
WHERE \ |
||||
`problem_id` = __PROBLEM_ID__ AND `status` = "Accepted" AND `type` = 0 \ |
||||
ORDER BY `total_time` DESC \ |
||||
', |
||||
shortest: |
||||
' \ |
||||
SELECT \ |
||||
DISTINCT(`user_id`) AS `user_id`, \ |
||||
( \ |
||||
SELECT \ |
||||
`id` \ |
||||
FROM `judge_state` `inner_table` \ |
||||
WHERE `problem_id` = `outer_table`.`problem_id` AND `user_id` = `outer_table`.`user_id` AND `status` = "Accepted" AND `type` = 0 \ |
||||
ORDER BY `code_length` ASC \ |
||||
LIMIT 1 \ |
||||
) AS `id`, \ |
||||
( \ |
||||
SELECT \ |
||||
`code_length` \ |
||||
FROM `judge_state` `inner_table` \ |
||||
WHERE `problem_id` = `outer_table`.`problem_id` AND `user_id` = `outer_table`.`user_id` AND `status` = "Accepted" AND `type` = 0 \ |
||||
ORDER BY `code_length` ASC \ |
||||
LIMIT 1 \ |
||||
) AS `code_length` \ |
||||
FROM `judge_state` `outer_table` \ |
||||
WHERE \ |
||||
`problem_id` = __PROBLEM_ID__ AND `status` = "Accepted" AND `type` = 0 \ |
||||
ORDER BY `code_length` ASC \ |
||||
', |
||||
longest: |
||||
' \ |
||||
SELECT \ |
||||
DISTINCT(`user_id`) AS `user_id`, \ |
||||
( \ |
||||
SELECT \ |
||||
`id` \ |
||||
FROM `judge_state` `inner_table` \ |
||||
WHERE `problem_id` = `outer_table`.`problem_id` AND `user_id` = `outer_table`.`user_id` AND `status` = "Accepted" AND `type` = 0 \ |
||||
ORDER BY `code_length` DESC \ |
||||
LIMIT 1 \ |
||||
) AS `id`, \ |
||||
( \ |
||||
SELECT \ |
||||
`code_length` \ |
||||
FROM `judge_state` `inner_table` \ |
||||
WHERE `problem_id` = `outer_table`.`problem_id` AND `user_id` = `outer_table`.`user_id` AND `status` = "Accepted" AND `type` = 0 \ |
||||
ORDER BY `code_length` DESC \ |
||||
LIMIT 1 \ |
||||
) AS `code_length` \ |
||||
FROM `judge_state` `outer_table` \ |
||||
WHERE \ |
||||
`problem_id` = __PROBLEM_ID__ AND `status` = "Accepted" AND `type` = 0 \ |
||||
ORDER BY `code_length` DESC \ |
||||
', |
||||
earliest: |
||||
' \ |
||||
SELECT \ |
||||
DISTINCT(`user_id`) AS `user_id`, \ |
||||
( \ |
||||
SELECT \ |
||||
`id` \ |
||||
FROM `judge_state` `inner_table` \ |
||||
WHERE `problem_id` = `outer_table`.`problem_id` AND `user_id` = `outer_table`.`user_id` AND `status` = "Accepted" AND `type` = 0 \ |
||||
ORDER BY `submit_time` ASC \ |
||||
LIMIT 1 \ |
||||
) AS `id`, \ |
||||
( \ |
||||
SELECT \ |
||||
`submit_time` \ |
||||
FROM `judge_state` `inner_table` \ |
||||
WHERE `problem_id` = `outer_table`.`problem_id` AND `user_id` = `outer_table`.`user_id` AND `status` = "Accepted" AND `type` = 0 \ |
||||
ORDER BY `submit_time` ASC \ |
||||
LIMIT 1 \ |
||||
) AS `submit_time` \ |
||||
FROM `judge_state` `outer_table` \ |
||||
WHERE \ |
||||
`problem_id` = __PROBLEM_ID__ AND `status` = "Accepted" AND `type` = 0 \ |
||||
ORDER BY `submit_time` ASC \ |
||||
', |
||||
min: |
||||
' \ |
||||
SELECT \ |
||||
DISTINCT(`user_id`) AS `user_id`, \ |
||||
( \ |
||||
SELECT \ |
||||
`id` \ |
||||
FROM `judge_state` `inner_table` \ |
||||
WHERE `problem_id` = `outer_table`.`problem_id` AND `user_id` = `outer_table`.`user_id` AND `status` = "Accepted" AND `type` = 0 \ |
||||
ORDER BY `max_memory` ASC \ |
||||
LIMIT 1 \ |
||||
) AS `id`, \ |
||||
( \ |
||||
SELECT \ |
||||
`max_memory` \ |
||||
FROM `judge_state` `inner_table` \ |
||||
WHERE `problem_id` = `outer_table`.`problem_id` AND `user_id` = `outer_table`.`user_id` AND `status` = "Accepted" AND `type` = 0 \ |
||||
ORDER BY `max_memory` ASC \ |
||||
LIMIT 1 \ |
||||
) AS `max_memory` \ |
||||
FROM `judge_state` `outer_table` \ |
||||
WHERE \ |
||||
`problem_id` = __PROBLEM_ID__ AND `status` = "Accepted" AND `type` = 0 \ |
||||
ORDER BY `max_memory` ASC \ |
||||
', |
||||
max: |
||||
' \ |
||||
SELECT \ |
||||
DISTINCT(`user_id`) AS `user_id`, \ |
||||
( \ |
||||
SELECT \ |
||||
`id` \ |
||||
FROM `judge_state` `inner_table` \ |
||||
WHERE `problem_id` = `outer_table`.`problem_id` AND `user_id` = `outer_table`.`user_id` AND `status` = "Accepted" AND `type` = 0 \ |
||||
ORDER BY `max_memory` ASC \ |
||||
LIMIT 1 \ |
||||
) AS `id`, \ |
||||
( \ |
||||
SELECT \ |
||||
`max_memory` \ |
||||
FROM `judge_state` `inner_table` \ |
||||
WHERE `problem_id` = `outer_table`.`problem_id` AND `user_id` = `outer_table`.`user_id` AND `status` = "Accepted" AND `type` = 0 \ |
||||
ORDER BY `max_memory` ASC \ |
||||
LIMIT 1 \ |
||||
) AS `max_memory` \ |
||||
FROM `judge_state` `outer_table` \ |
||||
WHERE \ |
||||
`problem_id` = __PROBLEM_ID__ AND `status` = "Accepted" AND `type` = 0 \ |
||||
ORDER BY `max_memory` DESC \ |
||||
' |
||||
}; |
||||
|
||||
let Sequelize = require('sequelize'); |
||||
let db = syzoj.db; |
||||
|
||||
let User = syzoj.model('user'); |
||||
let File = syzoj.model('file'); |
||||
const fs = require('fs-extra'); |
||||
const path = require('path'); |
||||
|
||||
let model = db.define('problem', { |
||||
id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, |
||||
|
||||
title: { type: Sequelize.STRING(80) }, |
||||
user_id: { |
||||
type: Sequelize.INTEGER, |
||||
references: { |
||||
model: 'user', |
||||
key: 'id' |
||||
} |
||||
}, |
||||
publicizer_id: { |
||||
type: Sequelize.INTEGER, |
||||
references: { |
||||
model: 'user', |
||||
key: 'id' |
||||
} |
||||
}, |
||||
is_anonymous: { type: Sequelize.BOOLEAN }, |
||||
|
||||
description: { type: Sequelize.TEXT }, |
||||
input_format: { type: Sequelize.TEXT }, |
||||
output_format: { type: Sequelize.TEXT }, |
||||
example: { type: Sequelize.TEXT }, |
||||
limit_and_hint: { type: Sequelize.TEXT }, |
||||
|
||||
time_limit: { type: Sequelize.INTEGER }, |
||||
memory_limit: { type: Sequelize.INTEGER }, |
||||
|
||||
additional_file_id: { type: Sequelize.INTEGER }, |
||||
|
||||
ac_num: { type: Sequelize.INTEGER }, |
||||
submit_num: { type: Sequelize.INTEGER }, |
||||
is_public: { type: Sequelize.BOOLEAN }, |
||||
|
||||
file_io: { type: Sequelize.BOOLEAN }, |
||||
file_io_input_name: { type: Sequelize.TEXT }, |
||||
file_io_output_name: { type: Sequelize.TEXT }, |
||||
|
||||
publicize_time: { type: Sequelize.DATE }, |
||||
|
||||
type: { |
||||
type: Sequelize.ENUM, |
||||
values: ['traditional', 'submit-answer', 'interaction'] |
||||
} |
||||
}, { |
||||
timestamps: false, |
||||
tableName: 'problem', |
||||
indexes: [ |
||||
{ |
||||
fields: ['title'], |
||||
}, |
||||
{ |
||||
fields: ['user_id'], |
||||
}, |
||||
{ |
||||
fields: ['publicize_time'], |
||||
}, |
||||
] |
||||
}); |
||||
|
||||
let Model = require('./common'); |
||||
class Problem extends Model { |
||||
static async create(val) { |
||||
return Problem.fromRecord(Problem.model.build(Object.assign({ |
||||
title: '', |
||||
user_id: '', |
||||
publicizer_id: '', |
||||
is_anonymous: false, |
||||
description: '', |
||||
|
||||
input_format: '', |
||||
output_format: '', |
||||
example: '', |
||||
limit_and_hint: '', |
||||
|
||||
time_limit: syzoj.config.default.problem.time_limit, |
||||
memory_limit: syzoj.config.default.problem.memory_limit, |
||||
|
||||
ac_num: 0, |
||||
submit_num: 0, |
||||
is_public: false, |
||||
|
||||
file_io: false, |
||||
file_io_input_name: '', |
||||
file_io_output_name: '', |
||||
|
||||
type: 'traditional' |
||||
}, val))); |
||||
} |
||||
|
||||
async loadRelationships() { |
||||
this.user = await User.fromID(this.user_id); |
||||
this.publicizer = await User.fromID(this.publicizer_id); |
||||
this.additional_file = await File.fromID(this.additional_file_id); |
||||
} |
||||
|
||||
async isAllowedEditBy(user) { |
||||
if (!user) return false; |
||||
if (await user.hasPrivilege('manage_problem')) return true; |
||||
return this.user_id === user.id; |
||||
} |
||||
|
||||
async isAllowedUseBy(user) { |
||||
if (this.is_public) return true; |
||||
if (!user) return false; |
||||
if (await user.hasPrivilege('manage_problem')) return true; |
||||
return this.user_id === user.id; |
||||
} |
||||
|
||||
async isAllowedManageBy(user) { |
||||
if (!user) return false; |
||||
if (await user.hasPrivilege('manage_problem')) return true; |
||||
return user.is_admin; |
||||
} |
||||
|
||||
getTestdataPath() { |
||||
return syzoj.utils.resolvePath(syzoj.config.upload_dir, 'testdata', this.id.toString()); |
||||
} |
||||
|
||||
getTestdataArchivePath() { |
||||
return syzoj.utils.resolvePath(syzoj.config.upload_dir, 'testdata-archive', this.id.toString() + '.zip'); |
||||
} |
||||
|
||||
async updateTestdata(path, noLimit) { |
||||
await syzoj.utils.lock(['Problem::Testdata', this.id], async () => { |
||||
let p7zip = new (require('node-7z')); |
||||
|
||||
let unzipSize = 0, unzipCount; |
||||
await p7zip.list(path).progress(files => { |
||||
unzipCount = files.length; |
||||
for (let file of files) unzipSize += file.size; |
||||
}); |
||||
if (!noLimit && unzipCount > syzoj.config.limit.testdata_filecount) throw new ErrorMessage('数据包中的文件太多。'); |
||||
if (!noLimit && unzipSize > syzoj.config.limit.testdata) throw new ErrorMessage('数据包太大。'); |
||||
|
||||
let dir = this.getTestdataPath(); |
||||
await fs.remove(dir); |
||||
await fs.ensureDir(dir); |
||||
|
||||
let execFileAsync = Promise.promisify(require('child_process').execFile); |
||||
await execFileAsync(__dirname + '/../bin/unzip', ['-j', '-o', '-d', dir, path]); |
||||
await fs.move(path, this.getTestdataArchivePath(), { overwrite: true }); |
||||
}); |
||||
} |
||||
|
||||
async uploadTestdataSingleFile(filename, filepath, size, noLimit) { |
||||
await syzoj.utils.lock(['Promise::Testdata', this.id], async () => { |
||||
let dir = this.getTestdataPath(); |
||||
await fs.ensureDir(dir); |
||||
|
||||
let oldSize = 0, list = await this.listTestdata(), replace = false, oldCount = 0; |
||||
if (list) { |
||||
oldCount = list.files.length; |
||||
for (let file of list.files) { |
||||
if (file.filename !== filename) oldSize += file.size; |
||||
else replace = true; |
||||
} |
||||
} |
||||
|
||||
if (!noLimit && oldSize + size > syzoj.config.limit.testdata) throw new ErrorMessage('数据包太大。'); |
||||
if (!noLimit && oldCount + !replace > syzoj.config.limit.testdata_filecount) throw new ErrorMessage('数据包中的文件太多。'); |
||||
|
||||
await fs.move(filepath, path.join(dir, filename), { overwrite: true }); |
||||
|
||||
let execFileAsync = Promise.promisify(require('child_process').execFile); |
||||
try { await execFileAsync('dos2unix', [path.join(dir, filename)]); } catch (e) {} |
||||
|
||||
await fs.remove(this.getTestdataArchivePath()); |
||||
}); |
||||
} |
||||
|
||||
async deleteTestdataSingleFile(filename) { |
||||
await syzoj.utils.lock(['Promise::Testdata', this.id], async () => { |
||||
await fs.remove(path.join(this.getTestdataPath(), filename)); |
||||
await fs.remove(this.getTestdataArchivePath()); |
||||
}); |
||||
} |
||||
|
||||
async makeTestdataZip() { |
||||
await syzoj.utils.lock(['Promise::Testdata', this.id], async () => { |
||||
let dir = this.getTestdataPath(); |
||||
if (!await syzoj.utils.isDir(dir)) throw new ErrorMessage('无测试数据。'); |
||||
|
||||
let p7zip = new (require('node-7z')); |
||||
|
||||
let list = await this.listTestdata(), pathlist = list.files.map(file => path.join(dir, file.filename)); |
||||
if (!pathlist.length) throw new ErrorMessage('无测试数据。'); |
||||
await fs.ensureDir(path.resolve(this.getTestdataArchivePath(), '..')); |
||||
await p7zip.add(this.getTestdataArchivePath(), pathlist); |
||||
}); |
||||
} |
||||
|
||||
async hasSpecialJudge() { |
||||
try { |
||||
let dir = this.getTestdataPath(); |
||||
let list = await fs.readdir(dir); |
||||
return list.includes('spj.js') || list.find(x => x.startsWith('spj_')) !== undefined; |
||||
} catch (e) { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
async listTestdata() { |
||||
try { |
||||
let dir = this.getTestdataPath(); |
||||
let list = await fs.readdir(dir); |
||||
list = await list.mapAsync(async x => { |
||||
let stat = await fs.stat(path.join(dir, x)); |
||||
if (!stat.isFile()) return undefined; |
||||
return { |
||||
filename: x, |
||||
size: stat.size |
||||
}; |
||||
}); |
||||
|
||||
list = list.filter(x => x); |
||||
|
||||
let res = { |
||||
files: list, |
||||
zip: null |
||||
}; |
||||
|
||||
try { |
||||
let stat = await fs.stat(this.getTestdataArchivePath()); |
||||
if (stat.isFile()) { |
||||
res.zip = { |
||||
size: stat.size |
||||
}; |
||||
} |
||||
} catch (e) { |
||||
if (list) { |
||||
res.zip = { |
||||
size: null |
||||
}; |
||||
} |
||||
} |
||||
|
||||
return res; |
||||
} catch (e) { |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
async updateFile(path, type, noLimit) { |
||||
let file = await File.upload(path, type, noLimit); |
||||
|
||||
if (type === 'additional_file') { |
||||
this.additional_file_id = file.id; |
||||
} |
||||
|
||||
await this.save(); |
||||
} |
||||
|
||||
async validate() { |
||||
if (this.time_limit <= 0) return 'Invalid time limit'; |
||||
if (this.time_limit > syzoj.config.limit.time_limit) return 'Time limit too large'; |
||||
if (this.memory_limit <= 0) return 'Invalid memory limit'; |
||||
if (this.memory_limit > syzoj.config.limit.memory_limit) return 'Memory limit too large'; |
||||
if (!['traditional', 'submit-answer', 'interaction'].includes(this.type)) return 'Invalid problem type'; |
||||
|
||||
if (this.type === 'traditional') { |
||||
let filenameRE = /^[\w \-\+\.]*$/; |
||||
if (this.file_io_input_name && !filenameRE.test(this.file_io_input_name)) return 'Invalid input file name'; |
||||
if (this.file_io_output_name && !filenameRE.test(this.file_io_output_name)) return 'Invalid output file name'; |
||||
|
||||
if (this.file_io) { |
||||
if (!this.file_io_input_name) return 'No input file name'; |
||||
if (!this.file_io_output_name) return 'No output file name'; |
||||
} |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
async getJudgeState(user, acFirst) { |
||||
if (!user) return null; |
||||
let JudgeState = syzoj.model('judge_state'); |
||||
|
||||
let where = { |
||||
user_id: user.id, |
||||
problem_id: this.id |
||||
}; |
||||
|
||||
if (acFirst) { |
||||
where.status = 'Accepted'; |
||||
|
||||
let state = await JudgeState.findOne({ |
||||
where: where, |
||||
order: [['submit_time', 'desc']] |
||||
}); |
||||
|
||||
if (state) return state; |
||||
} |
||||
|
||||
if (where.status) delete where.status; |
||||
|
||||
return await JudgeState.findOne({ |
||||
where: where, |
||||
order: [['submit_time', 'desc']] |
||||
}); |
||||
} |
||||
|
||||
async resetSubmissionCount() { |
||||
let JudgeState = syzoj.model('judge_state'); |
||||
await syzoj.utils.lock(['Problem::resetSubmissionCount', this.id], async () => { |
||||
this.submit_num = await JudgeState.count({ problem_id: this.id, type: { $not: 1 } }); |
||||
this.ac_num = await JudgeState.count({ score: 100, problem_id: this.id, type: { $not: 1 } }); |
||||
await this.save(); |
||||
}); |
||||
} |
||||
|
||||
// type: fastest / slowest / shortest / longest / earliest
|
||||
async countStatistics(type) { |
||||
let statement = statisticsStatements[type]; |
||||
if (!statement) return null; |
||||
|
||||
statement = statement.replace('__PROBLEM_ID__', this.id); |
||||
return await db.countQuery(statement); |
||||
} |
||||
|
||||
// type: fastest / slowest / shortest / longest / earliest
|
||||
async getStatistics(type, paginate) { |
||||
let statistics = { |
||||
type: type, |
||||
judge_state: null, |
||||
scoreDistribution: null, |
||||
prefixSum: null, |
||||
suffixSum: null |
||||
}; |
||||
|
||||
let statement = statisticsStatements[type]; |
||||
if (!statement) return null; |
||||
|
||||
statement = statement.replace('__PROBLEM_ID__', this.id); |
||||
|
||||
let a; |
||||
if (!paginate.pageCnt) a = []; |
||||
else a = (await db.query(statement + `LIMIT ${paginate.perPage} OFFSET ${(paginate.currPage - 1) * paginate.perPage}`))[0]; |
||||
|
||||
let JudgeState = syzoj.model('judge_state'); |
||||
statistics.judge_state = await a.mapAsync(async x => JudgeState.fromID(x.id)); |
||||
|
||||
a = (await db.query('SELECT `score`, COUNT(*) AS `count` FROM `judge_state` WHERE `problem_id` = __PROBLEM_ID__ AND `type` = 0 AND `pending` = 0 GROUP BY `score`'.replace('__PROBLEM_ID__', this.id)))[0]; |
||||
|
||||
let scoreCount = []; |
||||
for (let score of a) { |
||||
score.score = Math.min(Math.round(score.score), 100); |
||||
scoreCount[score.score] = score.count; |
||||
} |
||||
if (scoreCount[0] === undefined) scoreCount[0] = 0; |
||||
if (scoreCount[100] === undefined) scoreCount[100] = 0; |
||||
|
||||
statistics.scoreDistribution = []; |
||||
for (let i = 0; i < scoreCount.length; i++) { |
||||
if (scoreCount[i] !== undefined) statistics.scoreDistribution.push({ score: i, count: scoreCount[i] }); |
||||
} |
||||
|
||||
statistics.prefixSum = JSON.parse(JSON.stringify(statistics.scoreDistribution)); |
||||
statistics.suffixSum = JSON.parse(JSON.stringify(statistics.scoreDistribution)); |
||||
|
||||
for (let i = 1; i < statistics.prefixSum.length; i++) { |
||||
statistics.prefixSum[i].count += statistics.prefixSum[i - 1].count; |
||||
} |
||||
|
||||
for (let i = statistics.prefixSum.length - 1; i >= 1; i--) { |
||||
statistics.suffixSum[i - 1].count += statistics.suffixSum[i].count; |
||||
} |
||||
|
||||
return statistics; |
||||
} |
||||
|
||||
async getTags() { |
||||
let ProblemTagMap = syzoj.model('problem_tag_map'); |
||||
let maps = await ProblemTagMap.query(null, { |
||||
problem_id: this.id |
||||
}); |
||||
|
||||
let ProblemTag = syzoj.model('problem_tag'); |
||||
let res = await maps.mapAsync(async map => { |
||||
return ProblemTag.fromID(map.tag_id); |
||||
}); |
||||
|
||||
res.sort((a, b) => { |
||||
return a.color > b.color ? 1 : -1; |
||||
}); |
||||
|
||||
return res; |
||||
} |
||||
|
||||
async setTags(newTagIDs) { |
||||
let ProblemTagMap = syzoj.model('problem_tag_map'); |
||||
|
||||
let oldTagIDs = (await this.getTags()).map(x => x.id); |
||||
|
||||
let delTagIDs = oldTagIDs.filter(x => !newTagIDs.includes(x)); |
||||
let addTagIDs = newTagIDs.filter(x => !oldTagIDs.includes(x)); |
||||
|
||||
for (let tagID of delTagIDs) { |
||||
let map = await ProblemTagMap.findOne({ |
||||
where: { |
||||
problem_id: this.id, |
||||
tag_id: tagID |
||||
} |
||||
}); |
||||
|
||||
await map.destroy(); |
||||
} |
||||
|
||||
for (let tagID of addTagIDs) { |
||||
let map = await ProblemTagMap.create({ |
||||
problem_id: this.id, |
||||
tag_id: tagID |
||||
}); |
||||
|
||||
await map.save(); |
||||
} |
||||
} |
||||
|
||||
async changeID(id) { |
||||
id = parseInt(id); |
||||
await db.query('UPDATE `problem` SET `id` = ' + id + ' WHERE `id` = ' + this.id); |
||||
await db.query('UPDATE `judge_state` SET `problem_id` = ' + id + ' WHERE `problem_id` = ' + this.id); |
||||
await db.query('UPDATE `problem_tag_map` SET `problem_id` = ' + id + ' WHERE `problem_id` = ' + this.id); |
||||
await db.query('UPDATE `article` SET `problem_id` = ' + id + ' WHERE `problem_id` = ' + this.id); |
||||
|
||||
let Contest = syzoj.model('contest'); |
||||
let contests = await Contest.all(); |
||||
for (let contest of contests) { |
||||
let problemIDs = await contest.getProblems(); |
||||
|
||||
let flag = false; |
||||
for (let i in problemIDs) { |
||||
if (problemIDs[i] === this.id) { |
||||
problemIDs[i] = id; |
||||
flag = true; |
||||
} |
||||
} |
||||
|
||||
if (flag) { |
||||
await contest.setProblemsNoCheck(problemIDs); |
||||
await contest.save(); |
||||
} |
||||
} |
||||
|
||||
let oldTestdataDir = this.getTestdataPath(), oldTestdataZip = this.getTestdataArchivePath(); |
||||
|
||||
this.id = id; |
||||
|
||||
// Move testdata
|
||||
let newTestdataDir = this.getTestdataPath(), newTestdataZip = this.getTestdataArchivePath(); |
||||
if (await syzoj.utils.isDir(oldTestdataDir)) { |
||||
await fs.move(oldTestdataDir, newTestdataDir); |
||||
} |
||||
|
||||
if (await syzoj.utils.isFile(oldTestdataZip)) { |
||||
await fs.move(oldTestdataZip, newTestdataZip); |
||||
} |
||||
|
||||
await this.save(); |
||||
} |
||||
|
||||
async delete() { |
||||
let oldTestdataDir = this.getTestdataPath(), oldTestdataZip = this.getTestdataPath(); |
||||
await fs.remove(oldTestdataDir); |
||||
await fs.remove(oldTestdataZip); |
||||
|
||||
let JudgeState = syzoj.model('judge_state'); |
||||
let submissions = await JudgeState.query(null, { problem_id: this.id }), submitCnt = {}, acUsers = new Set(); |
||||
for (let sm of submissions) { |
||||
if (sm.status === 'Accepted') acUsers.add(sm.user_id); |
||||
if (!submitCnt[sm.user_id]) { |
||||
submitCnt[sm.user_id] = 1; |
||||
} else { |
||||
submitCnt[sm.user_id]++; |
||||
} |
||||
} |
||||
|
||||
for (let u in submitCnt) { |
||||
let user = await User.fromID(u); |
||||
user.submit_num -= submitCnt[u]; |
||||
if (acUsers.has(parseInt(u))) user.ac_num--; |
||||
await user.save(); |
||||
} |
||||
|
||||
await db.query('DELETE FROM `problem` WHERE `id` = ' + this.id); |
||||
await db.query('DELETE FROM `judge_state` WHERE `problem_id` = ' + this.id); |
||||
await db.query('DELETE FROM `problem_tag_map` WHERE `problem_id` = ' + this.id); |
||||
await db.query('DELETE FROM `article` WHERE `problem_id` = ' + this.id); |
||||
} |
||||
|
||||
getModel() { return model; } |
||||
} |
||||
|
||||
Problem.model = model; |
||||
|
||||
module.exports = Problem; |
@ -0,0 +1,595 @@
|
||||
import * as TypeORM from "typeorm"; |
||||
import Model from "./common"; |
||||
|
||||
declare var syzoj, ErrorMessage: any; |
||||
|
||||
import User from "./user"; |
||||
import File from "./file"; |
||||
import JudgeState from "./judge_state"; |
||||
import Contest from "./contest"; |
||||
import ProblemTag from "./problem_tag"; |
||||
import ProblemTagMap from "./problem_tag_map"; |
||||
import SubmissionStatistics, { StatisticsType } from "./submission_statistics"; |
||||
|
||||
import * as fs from "fs-extra"; |
||||
import * as path from "path"; |
||||
import * as util from "util"; |
||||
import * as LRUCache from "lru-cache"; |
||||
import * as DeepCopy from "deepcopy"; |
||||
|
||||
const problemTagCache = new LRUCache<number, number[]>({ |
||||
max: syzoj.config.db.cache_size |
||||
}); |
||||
|
||||
enum ProblemType { |
||||
Traditional = "traditional", |
||||
SubmitAnswer = "submit-answer", |
||||
Interaction = "interaction" |
||||
} |
||||
|
||||
const statisticsTypes = { |
||||
fastest: ['total_time', 'ASC'], |
||||
slowest: ['total_time', 'DESC'], |
||||
shortest: ['code_length', 'ASC'], |
||||
longest: ['code_length', 'DESC'], |
||||
min: ['max_memory', 'ASC'], |
||||
max: ['max_memory', 'DESC'], |
||||
earliest: ['submit_time', 'ASC'] |
||||
}; |
||||
|
||||
const statisticsCodeOnly = ["fastest", "slowest", "min", "max"]; |
||||
|
||||
@TypeORM.Entity() |
||||
export default class Problem extends Model { |
||||
static cache = true; |
||||
|
||||
@TypeORM.PrimaryGeneratedColumn() |
||||
id: number; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "varchar", length: 80 }) |
||||
title: string; |
||||
|
||||
@TypeORM.Index() |
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
user_id: number; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
publicizer_id: number; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "boolean" }) |
||||
is_anonymous: boolean; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "text" }) |
||||
description: string; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "text" }) |
||||
input_format: string; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "text" }) |
||||
output_format: string; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "text" }) |
||||
example: string; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "text" }) |
||||
limit_and_hint: string; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
time_limit: number; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
memory_limit: number; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
additional_file_id: number; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
ac_num: number; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
submit_num: number; |
||||
|
||||
@TypeORM.Index() |
||||
@TypeORM.Column({ nullable: true, type: "boolean" }) |
||||
is_public: boolean; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "boolean" }) |
||||
file_io: boolean; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "text" }) |
||||
file_io_input_name: string; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "text" }) |
||||
file_io_output_name: string; |
||||
|
||||
@TypeORM.Index() |
||||
@TypeORM.Column({ nullable: true, type: "datetime" }) |
||||
publicize_time: Date; |
||||
|
||||
@TypeORM.Column({ nullable: true, |
||||
type: "enum", |
||||
enum: ProblemType, |
||||
default: ProblemType.Traditional |
||||
}) |
||||
type: ProblemType; |
||||
|
||||
user?: User; |
||||
publicizer?: User; |
||||
additional_file?: File; |
||||
|
||||
async loadRelationships() { |
||||
this.user = await User.findById(this.user_id); |
||||
this.publicizer = await User.findById(this.publicizer_id); |
||||
this.additional_file = await File.findById(this.additional_file_id); |
||||
} |
||||
|
||||
async isAllowedEditBy(user) { |
||||
if (!user) return false; |
||||
if (await user.hasPrivilege('manage_problem')) return true; |
||||
return this.user_id === user.id; |
||||
} |
||||
|
||||
async isAllowedUseBy(user) { |
||||
if (this.is_public) return true; |
||||
if (!user) return false; |
||||
if (await user.hasPrivilege('manage_problem')) return true; |
||||
return this.user_id === user.id; |
||||
} |
||||
|
||||
async isAllowedManageBy(user) { |
||||
if (!user) return false; |
||||
if (await user.hasPrivilege('manage_problem')) return true; |
||||
return user.is_admin; |
||||
} |
||||
|
||||
getTestdataPath() { |
||||
return syzoj.utils.resolvePath(syzoj.config.upload_dir, 'testdata', this.id.toString()); |
||||
} |
||||
|
||||
getTestdataArchivePath() { |
||||
return syzoj.utils.resolvePath(syzoj.config.upload_dir, 'testdata-archive', this.id.toString() + '.zip'); |
||||
} |
||||
|
||||
async updateTestdata(path, noLimit) { |
||||
await syzoj.utils.lock(['Problem::Testdata', this.id], async () => { |
||||
let p7zip = new (require('node-7z')); |
||||
|
||||
let unzipSize = 0, unzipCount; |
||||
await p7zip.list(path).progress(files => { |
||||
unzipCount = files.length; |
||||
for (let file of files) unzipSize += file.size; |
||||
}); |
||||
if (!noLimit && unzipCount > syzoj.config.limit.testdata_filecount) throw new ErrorMessage('数据包中的文件太多。'); |
||||
if (!noLimit && unzipSize > syzoj.config.limit.testdata) throw new ErrorMessage('数据包太大。'); |
||||
|
||||
let dir = this.getTestdataPath(); |
||||
await fs.remove(dir); |
||||
await fs.ensureDir(dir); |
||||
|
||||
let execFileAsync = util.promisify(require('child_process').execFile); |
||||
await execFileAsync(__dirname + '/../bin/unzip', ['-j', '-o', '-d', dir, path]); |
||||
await fs.move(path, this.getTestdataArchivePath(), { overwrite: true }); |
||||
}); |
||||
} |
||||
|
||||
async uploadTestdataSingleFile(filename, filepath, size, noLimit) { |
||||
await syzoj.utils.lock(['Promise::Testdata', this.id], async () => { |
||||
let dir = this.getTestdataPath(); |
||||
await fs.ensureDir(dir); |
||||
|
||||
let oldSize = 0, list = await this.listTestdata(), replace = false, oldCount = 0; |
||||
if (list) { |
||||
oldCount = list.files.length; |
||||
for (let file of list.files) { |
||||
if (file.filename !== filename) oldSize += file.size; |
||||
else replace = true; |
||||
} |
||||
} |
||||
|
||||
if (!noLimit && oldSize + size > syzoj.config.limit.testdata) throw new ErrorMessage('数据包太大。'); |
||||
if (!noLimit && oldCount + (!replace as any as number) > syzoj.config.limit.testdata_filecount) throw new ErrorMessage('数据包中的文件太多。'); |
||||
|
||||
await fs.move(filepath, path.join(dir, filename), { overwrite: true }); |
||||
|
||||
let execFileAsync = util.promisify(require('child_process').execFile); |
||||
try { await execFileAsync('dos2unix', [path.join(dir, filename)]); } catch (e) {} |
||||
|
||||
await fs.remove(this.getTestdataArchivePath()); |
||||
}); |
||||
} |
||||
|
||||
async deleteTestdataSingleFile(filename) { |
||||
await syzoj.utils.lock(['Promise::Testdata', this.id], async () => { |
||||
await fs.remove(path.join(this.getTestdataPath(), filename)); |
||||
await fs.remove(this.getTestdataArchivePath()); |
||||
}); |
||||
} |
||||
|
||||
async makeTestdataZip() { |
||||
await syzoj.utils.lock(['Promise::Testdata', this.id], async () => { |
||||
let dir = this.getTestdataPath(); |
||||
if (!await syzoj.utils.isDir(dir)) throw new ErrorMessage('无测试数据。'); |
||||
|
||||
let p7zip = new (require('node-7z')); |
||||
|
||||
let list = await this.listTestdata(), pathlist = list.files.map(file => path.join(dir, file.filename)); |
||||
if (!pathlist.length) throw new ErrorMessage('无测试数据。'); |
||||
await fs.ensureDir(path.resolve(this.getTestdataArchivePath(), '..')); |
||||
await p7zip.add(this.getTestdataArchivePath(), pathlist); |
||||
}); |
||||
} |
||||
|
||||
async hasSpecialJudge() { |
||||
try { |
||||
let dir = this.getTestdataPath(); |
||||
let list = await fs.readdir(dir); |
||||
return list.includes('spj.js') || list.find(x => x.startsWith('spj_')) !== undefined; |
||||
} catch (e) { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
async listTestdata() { |
||||
try { |
||||
let dir = this.getTestdataPath(); |
||||
let filenameList = await fs.readdir(dir); |
||||
let list = await Promise.all(filenameList.map(async x => { |
||||
let stat = await fs.stat(path.join(dir, x)); |
||||
if (!stat.isFile()) return undefined; |
||||
return { |
||||
filename: x, |
||||
size: stat.size |
||||
}; |
||||
})); |
||||
|
||||
list = list.filter(x => x); |
||||
|
||||
let res = { |
||||
files: list, |
||||
zip: null |
||||
}; |
||||
|
||||
try { |
||||
let stat = await fs.stat(this.getTestdataArchivePath()); |
||||
if (stat.isFile()) { |
||||
res.zip = { |
||||
size: stat.size |
||||
}; |
||||
} |
||||
} catch (e) { |
||||
if (list) { |
||||
res.zip = { |
||||
size: null |
||||
}; |
||||
} |
||||
} |
||||
|
||||
return res; |
||||
} catch (e) { |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
async updateFile(path, type, noLimit) { |
||||
let file = await File.upload(path, type, noLimit); |
||||
|
||||
if (type === 'additional_file') { |
||||
this.additional_file_id = file.id; |
||||
} |
||||
|
||||
await this.save(); |
||||
} |
||||
|
||||
async validate() { |
||||
if (this.time_limit <= 0) return 'Invalid time limit'; |
||||
if (this.time_limit > syzoj.config.limit.time_limit) return 'Time limit too large'; |
||||
if (this.memory_limit <= 0) return 'Invalid memory limit'; |
||||
if (this.memory_limit > syzoj.config.limit.memory_limit) return 'Memory limit too large'; |
||||
if (!['traditional', 'submit-answer', 'interaction'].includes(this.type)) return 'Invalid problem type'; |
||||
|
||||
if (this.type === 'traditional') { |
||||
let filenameRE = /^[\w \-\+\.]*$/; |
||||
if (this.file_io_input_name && !filenameRE.test(this.file_io_input_name)) return 'Invalid input file name'; |
||||
if (this.file_io_output_name && !filenameRE.test(this.file_io_output_name)) return 'Invalid output file name'; |
||||
|
||||
if (this.file_io) { |
||||
if (!this.file_io_input_name) return 'No input file name'; |
||||
if (!this.file_io_output_name) return 'No output file name'; |
||||
} |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
async getJudgeState(user, acFirst) { |
||||
if (!user) return null; |
||||
|
||||
let where: any = { |
||||
user_id: user.id, |
||||
problem_id: this.id |
||||
}; |
||||
|
||||
if (acFirst) { |
||||
where.status = 'Accepted'; |
||||
|
||||
let state = await JudgeState.findOne({ |
||||
where: where, |
||||
order: { |
||||
submit_time: 'DESC' |
||||
} |
||||
}); |
||||
|
||||
if (state) return state; |
||||
} |
||||
|
||||
if (where.status) delete where.status; |
||||
|
||||
return await JudgeState.findOne({ |
||||
where: where, |
||||
order: { |
||||
submit_time: 'DESC' |
||||
} |
||||
}); |
||||
} |
||||
|
||||
async resetSubmissionCount() { |
||||
await syzoj.utils.lock(['Problem::resetSubmissionCount', this.id], async () => { |
||||
this.submit_num = await JudgeState.count({ problem_id: this.id, type: TypeORM.Not(1) }); |
||||
this.ac_num = await JudgeState.count({ score: 100, problem_id: this.id, type: TypeORM.Not(1) }); |
||||
await this.save(); |
||||
}); |
||||
} |
||||
|
||||
async updateStatistics(user_id) { |
||||
await Promise.all(Object.keys(statisticsTypes).map(async type => { |
||||
if (this.type === ProblemType.SubmitAnswer && statisticsCodeOnly.includes(type)) return; |
||||
|
||||
await syzoj.utils.lock(['Problem::UpdateStatistics', this.id, type], async () => { |
||||
const [column, order] = statisticsTypes[type]; |
||||
const result = await JudgeState.createQueryBuilder() |
||||
.select([column, "id"]) |
||||
.where("user_id = :user_id", { user_id }) |
||||
.andWhere("status = :status", { status: "Accepted" }) |
||||
.andWhere("problem_id = :problem_id", { problem_id: this.id }) |
||||
.orderBy({ [column]: order }) |
||||
.take(1) |
||||
.getRawMany(); |
||||
const resultRow = result[0]; |
||||
|
||||
if (!resultRow || resultRow[column] == null) return; |
||||
|
||||
const baseColumns = { |
||||
user_id, |
||||
problem_id: this.id, |
||||
type: type as StatisticsType |
||||
}; |
||||
|
||||
let record = await SubmissionStatistics.findOne(baseColumns); |
||||
if (!record) { |
||||
record = SubmissionStatistics.create(baseColumns); |
||||
} |
||||
|
||||
record.key = resultRow[column]; |
||||
record.submission_id = resultRow["id"]; |
||||
|
||||
await record.save(); |
||||
}); |
||||
})); |
||||
} |
||||
|
||||
async countStatistics(type) { |
||||
return await SubmissionStatistics.count({ |
||||
problem_id: this.id, |
||||
type: type |
||||
}); |
||||
} |
||||
|
||||
async getStatistics(type, paginate) { |
||||
const entityManager = TypeORM.getManager(); |
||||
|
||||
let statistics = { |
||||
type: type, |
||||
judge_state: null, |
||||
scoreDistribution: null, |
||||
prefixSum: null, |
||||
suffixSum: null |
||||
}; |
||||
|
||||
const order = statisticsTypes[type][1]; |
||||
const ids = (await SubmissionStatistics.queryPage(paginate, { |
||||
problem_id: this.id, |
||||
type: type |
||||
}, { |
||||
'`key`': order |
||||
})).map(x => x.submission_id); |
||||
|
||||
statistics.judge_state = ids.length ? await JudgeState.createQueryBuilder() |
||||
.whereInIds(ids) |
||||
.orderBy(`FIELD(id,${ids.join(',')})`) |
||||
.getMany() |
||||
: []; |
||||
|
||||
JudgeState.createQueryBuilder() |
||||
.select('score') |
||||
.addSelect('COUNT(*)', 'count') |
||||
.where('problem_id = :problem_id', { problem_id: this.id }) |
||||
.andWhere('type = 0') |
||||
.andWhere('pending = false') |
||||
.groupBy('score') |
||||
.getRawMany() |
||||
let a = (await entityManager.query('SELECT `score`, COUNT(*) AS `count` FROM `judge_state` WHERE `problem_id` = __PROBLEM_ID__ AND `type` = 0 AND `pending` = 0 GROUP BY `score`'.replace('__PROBLEM_ID__', this.id.toString()))); |
||||
|
||||
let scoreCount = []; |
||||
for (let score of a) { |
||||
score.score = Math.min(Math.round(score.score), 100); |
||||
scoreCount[score.score] = score.count; |
||||
} |
||||
if (scoreCount[0] === undefined) scoreCount[0] = 0; |
||||
if (scoreCount[100] === undefined) scoreCount[100] = 0; |
||||
|
||||
if (a[null as any]) { |
||||
a[0] += a[null as any]; |
||||
delete a[null as any]; |
||||
} |
||||
|
||||
statistics.scoreDistribution = []; |
||||
for (let i = 0; i < scoreCount.length; i++) { |
||||
if (scoreCount[i] !== undefined) statistics.scoreDistribution.push({ score: i, count: parseInt(scoreCount[i]) }); |
||||
} |
||||
|
||||
statistics.prefixSum = DeepCopy(statistics.scoreDistribution); |
||||
statistics.suffixSum = DeepCopy(statistics.scoreDistribution); |
||||
|
||||
for (let i = 1; i < statistics.prefixSum.length; i++) { |
||||
statistics.prefixSum[i].count += statistics.prefixSum[i - 1].count; |
||||
} |
||||
|
||||
for (let i = statistics.prefixSum.length - 1; i >= 1; i--) { |
||||
statistics.suffixSum[i - 1].count += statistics.suffixSum[i].count; |
||||
} |
||||
|
||||
return statistics; |
||||
} |
||||
|
||||
async getTags() { |
||||
let tagIDs; |
||||
if (problemTagCache.has(this.id)) { |
||||
tagIDs = problemTagCache.get(this.id); |
||||
} else { |
||||
let maps = await ProblemTagMap.find({ |
||||
where: { |
||||
problem_id: this.id |
||||
} |
||||
}); |
||||
|
||||
tagIDs = maps.map(x => x.tag_id); |
||||
problemTagCache.set(this.id, tagIDs); |
||||
} |
||||
|
||||
let res = await (tagIDs as any).mapAsync(async tagID => { |
||||
return ProblemTag.findById(tagID); |
||||
}); |
||||
|
||||
res.sort((a, b) => { |
||||
return a.color > b.color ? 1 : -1; |
||||
}); |
||||
|
||||
return res; |
||||
} |
||||
|
||||
async setTags(newTagIDs) { |
||||
let oldTagIDs = (await this.getTags()).map(x => x.id); |
||||
|
||||
let delTagIDs = oldTagIDs.filter(x => !newTagIDs.includes(x)); |
||||
let addTagIDs = newTagIDs.filter(x => !oldTagIDs.includes(x)); |
||||
|
||||
for (let tagID of delTagIDs) { |
||||
let map = await ProblemTagMap.findOne({ |
||||
where: { |
||||
problem_id: this.id, |
||||
tag_id: tagID |
||||
} |
||||
}); |
||||
|
||||
await map.destroy(); |
||||
} |
||||
|
||||
for (let tagID of addTagIDs) { |
||||
let map = await ProblemTagMap.create({ |
||||
problem_id: this.id, |
||||
tag_id: tagID |
||||
}); |
||||
|
||||
await map.save(); |
||||
} |
||||
|
||||
problemTagCache.set(this.id, newTagIDs); |
||||
} |
||||
|
||||
async changeID(id) { |
||||
const entityManager = TypeORM.getManager(); |
||||
|
||||
id = parseInt(id); |
||||
await entityManager.query('UPDATE `problem` SET `id` = ' + id + ' WHERE `id` = ' + this.id); |
||||
await entityManager.query('UPDATE `judge_state` SET `problem_id` = ' + id + ' WHERE `problem_id` = ' + this.id); |
||||
await entityManager.query('UPDATE `problem_tag_map` SET `problem_id` = ' + id + ' WHERE `problem_id` = ' + this.id); |
||||
await entityManager.query('UPDATE `article` SET `problem_id` = ' + id + ' WHERE `problem_id` = ' + this.id); |
||||
await entityManager.query('UPDATE `submission_statistics` SET `problem_id` = ' + id + ' WHERE `problem_id` = ' + this.id); |
||||
|
||||
let contests = await Contest.find(); |
||||
for (let contest of contests) { |
||||
let problemIDs = await contest.getProblems(); |
||||
|
||||
let flag = false; |
||||
for (let i in problemIDs) { |
||||
if (problemIDs[i] === this.id) { |
||||
problemIDs[i] = id; |
||||
flag = true; |
||||
} |
||||
} |
||||
|
||||
if (flag) { |
||||
await contest.setProblemsNoCheck(problemIDs); |
||||
await contest.save(); |
||||
} |
||||
} |
||||
|
||||
let oldTestdataDir = this.getTestdataPath(), oldTestdataZip = this.getTestdataArchivePath(); |
||||
|
||||
const oldID = this.id; |
||||
this.id = id; |
||||
|
||||
// Move testdata
|
||||
let newTestdataDir = this.getTestdataPath(), newTestdataZip = this.getTestdataArchivePath(); |
||||
if (await syzoj.utils.isDir(oldTestdataDir)) { |
||||
await fs.move(oldTestdataDir, newTestdataDir); |
||||
} |
||||
|
||||
if (await syzoj.utils.isFile(oldTestdataZip)) { |
||||
await fs.move(oldTestdataZip, newTestdataZip); |
||||
} |
||||
|
||||
await this.save(); |
||||
|
||||
await Problem.deleteFromCache(oldID); |
||||
await problemTagCache.del(oldID); |
||||
} |
||||
|
||||
async delete() { |
||||
const entityManager = TypeORM.getManager(); |
||||
|
||||
let oldTestdataDir = this.getTestdataPath(), oldTestdataZip = this.getTestdataPath(); |
||||
await fs.remove(oldTestdataDir); |
||||
await fs.remove(oldTestdataZip); |
||||
|
||||
let submissions = await JudgeState.find({ |
||||
where: { |
||||
problem_id: this.id |
||||
} |
||||
}), submitCnt = {}, acUsers = new Set(); |
||||
for (let sm of submissions) { |
||||
if (sm.status === 'Accepted') acUsers.add(sm.user_id); |
||||
if (!submitCnt[sm.user_id]) { |
||||
submitCnt[sm.user_id] = 1; |
||||
} else { |
||||
submitCnt[sm.user_id]++; |
||||
} |
||||
} |
||||
|
||||
for (let u in submitCnt) { |
||||
let user = await User.findById(parseInt(u)); |
||||
user.submit_num -= submitCnt[u]; |
||||
if (acUsers.has(parseInt(u))) user.ac_num--; |
||||
await user.save(); |
||||
} |
||||
|
||||
problemTagCache.del(this.id); |
||||
|
||||
await entityManager.query('DELETE FROM `judge_state` WHERE `problem_id` = ' + this.id); |
||||
await entityManager.query('DELETE FROM `problem_tag_map` WHERE `problem_id` = ' + this.id); |
||||
await entityManager.query('DELETE FROM `article` WHERE `problem_id` = ' + this.id); |
||||
await entityManager.query('DELETE FROM `submission_statistics` WHERE `problem_id` = ' + this.id); |
||||
|
||||
await this.destroy(); |
||||
} |
||||
} |
@ -1,33 +0,0 @@
|
||||
let Sequelize = require('sequelize'); |
||||
let db = syzoj.db; |
||||
|
||||
let model = db.define('problem_tag', { |
||||
id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, |
||||
name: { type: Sequelize.STRING }, |
||||
color: { type: Sequelize.STRING }, |
||||
}, { |
||||
timestamps: false, |
||||
tableName: 'problem_tag', |
||||
indexes: [ |
||||
{ |
||||
unique: true, |
||||
fields: ['name'], |
||||
} |
||||
] |
||||
}); |
||||
|
||||
let Model = require('./common'); |
||||
class ProblemTag extends Model { |
||||
static async create(val) { |
||||
return ProblemTag.fromRecord(ProblemTag.model.build(Object.assign({ |
||||
name: '', |
||||
color: '' |
||||
}, val))); |
||||
} |
||||
|
||||
getModel() { return model; } |
||||
} |
||||
|
||||
ProblemTag.model = model; |
||||
|
||||
module.exports = ProblemTag; |
@ -0,0 +1,17 @@
|
||||
import * as TypeORM from "typeorm"; |
||||
import Model from "./common"; |
||||
|
||||
@TypeORM.Entity() |
||||
export default class ProblemTag extends Model { |
||||
static cache = true; |
||||
|
||||
@TypeORM.PrimaryGeneratedColumn() |
||||
id: number; |
||||
|
||||
@TypeORM.Index({ unique: true }) |
||||
@TypeORM.Column({ nullable: true, type: "varchar", length: 255 }) |
||||
name: string; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "varchar", length: 255 }) |
||||
color: string; |
||||
} |
@ -1,37 +0,0 @@
|
||||
let Sequelize = require('sequelize'); |
||||
let db = syzoj.db; |
||||
|
||||
let model = db.define('problem_tag_map', { |
||||
problem_id: { type: Sequelize.INTEGER, primaryKey: true }, |
||||
tag_id: { |
||||
type: Sequelize.INTEGER, |
||||
primaryKey: true |
||||
} |
||||
}, { |
||||
timestamps: false, |
||||
tableName: 'problem_tag_map', |
||||
indexes: [ |
||||
{ |
||||
fields: ['problem_id'] |
||||
}, |
||||
{ |
||||
fields: ['tag_id'] |
||||
} |
||||
] |
||||
}); |
||||
|
||||
let Model = require('./common'); |
||||
class ProblemTagMap extends Model { |
||||
static async create(val) { |
||||
return ProblemTagMap.fromRecord(ProblemTagMap.model.build(Object.assign({ |
||||
problem_id: 0, |
||||
tag_id: 0 |
||||
}, val))); |
||||
} |
||||
|
||||
getModel() { return model; } |
||||
} |
||||
|
||||
ProblemTagMap.model = model; |
||||
|
||||
module.exports = ProblemTagMap; |
@ -0,0 +1,13 @@
|
||||
import * as TypeORM from "typeorm"; |
||||
import Model from "./common"; |
||||
|
||||
@TypeORM.Entity() |
||||
export default class ProblemTagMap extends Model { |
||||
@TypeORM.Index() |
||||
@TypeORM.PrimaryColumn({ type: "integer" }) |
||||
problem_id: number; |
||||
|
||||
@TypeORM.Index() |
||||
@TypeORM.PrimaryColumn({ type: "integer" }) |
||||
tag_id: number; |
||||
} |
@ -1,53 +0,0 @@
|
||||
let Sequelize = require('sequelize'); |
||||
let db = syzoj.db; |
||||
const User = syzoj.model('user'); |
||||
const Contest = syzoj.model('contest'); |
||||
|
||||
let model = db.define('rating_calculation', { |
||||
id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, |
||||
contest_id: { type: Sequelize.INTEGER } |
||||
}, { |
||||
timestamps: false, |
||||
tableName: 'rating_calculation', |
||||
indexes: [ |
||||
{ |
||||
fields: ['contest_id'] |
||||
}, |
||||
] |
||||
}); |
||||
|
||||
let Model = require('./common'); |
||||
class RatingCalculation extends Model { |
||||
static async create(contest_id) { |
||||
return RatingCalculation.fromRecord(RatingCalculation.model.create({ contest_id: contest_id })); |
||||
} |
||||
|
||||
async loadRelationships() { |
||||
this.contest = await Contest.fromID(this.contest_id); |
||||
} |
||||
|
||||
getModel() { return model; } |
||||
|
||||
async delete() { |
||||
const RatingHistory = syzoj.model('rating_history'); |
||||
const histories = await RatingHistory.query(null, { |
||||
rating_calculation_id: this.id |
||||
}); |
||||
for (const history of histories) { |
||||
await history.loadRelationships(); |
||||
const user = history.user; |
||||
await history.destroy(); |
||||
const ratingItem = (await RatingHistory.findOne({ |
||||
where: { user_id: user.id }, |
||||
order: [['rating_calculation_id','DESC']] |
||||
})); |
||||
user.rating = ratingItem ? ratingItem.rating_after : syzoj.config.default.user.rating; |
||||
await user.save(); |
||||
} |
||||
await this.destroy(); |
||||
} |
||||
} |
||||
|
||||
RatingCalculation.model = model; |
||||
|
||||
module.exports = RatingCalculation; |
@ -0,0 +1,47 @@
|
||||
import * as TypeORM from "typeorm"; |
||||
import Model from "./common"; |
||||
|
||||
declare var syzoj: any; |
||||
|
||||
import Contest from "./contest"; |
||||
import RatingHistory from "./rating_history"; |
||||
|
||||
@TypeORM.Entity() |
||||
export default class RatingCalculation extends Model { |
||||
@TypeORM.PrimaryGeneratedColumn() |
||||
id: number; |
||||
|
||||
@TypeORM.Index({}) |
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
contest_id: number; |
||||
|
||||
contest?: Contest; |
||||
|
||||
async loadRelationships() { |
||||
this.contest = await Contest.findById(this.contest_id); |
||||
} |
||||
|
||||
async delete() { |
||||
const histories = await RatingHistory.find({ |
||||
where: { |
||||
rating_calculation_id: this.id |
||||
} |
||||
}); |
||||
for (const history of histories) { |
||||
await history.loadRelationships(); |
||||
const user = history.user; |
||||
await history.destroy(); |
||||
const ratingItem = (await RatingHistory.findOne({ |
||||
where: { |
||||
user_id: user.id |
||||
}, |
||||
order: { |
||||
rating_calculation_id: 'DESC' |
||||
} |
||||
})); |
||||
user.rating = ratingItem ? ratingItem.rating_after : syzoj.config.default.user.rating; |
||||
await user.save(); |
||||
} |
||||
await this.destroy(); |
||||
} |
||||
} |
@ -1,43 +0,0 @@
|
||||
let Sequelize = require('sequelize'); |
||||
const User = syzoj.model('user'); |
||||
let db = syzoj.db; |
||||
|
||||
let model = db.define('rating_history', { |
||||
rating_calculation_id: { type: Sequelize.INTEGER, primaryKey: true }, |
||||
user_id: { type: Sequelize.INTEGER, primaryKey: true }, |
||||
rating_after: { type: Sequelize.INTEGER }, |
||||
rank: { type: Sequelize.INTEGER }, |
||||
}, { |
||||
timestamps: false, |
||||
tableName: 'rating_history', |
||||
indexes: [ |
||||
{ |
||||
fields: ['rating_calculation_id'] |
||||
}, |
||||
{ |
||||
fields: ['user_id'] |
||||
}, |
||||
] |
||||
}); |
||||
|
||||
let Model = require('./common'); |
||||
class RatingHistory extends Model { |
||||
static async create(rating_calculation_id, user_id, rating, rank) { |
||||
return RatingHistory.fromRecord(RatingHistory.model.build({ |
||||
rating_calculation_id: rating_calculation_id, |
||||
user_id: user_id, |
||||
rating_after: rating, |
||||
rank: rank |
||||
})); |
||||
} |
||||
|
||||
async loadRelationships() { |
||||
this.user = await User.fromID(this.user_id); |
||||
} |
||||
|
||||
getModel() { return model; } |
||||
} |
||||
|
||||
RatingHistory.model = model; |
||||
|
||||
module.exports = RatingHistory; |
@ -0,0 +1,27 @@
|
||||
import * as TypeORM from "typeorm"; |
||||
import Model from "./common"; |
||||
|
||||
declare var syzoj: any; |
||||
|
||||
import User from "./user"; |
||||
|
||||
@TypeORM.Entity() |
||||
export default class RatingHistory extends Model { |
||||
@TypeORM.PrimaryColumn({ type: "integer" }) |
||||
rating_calculation_id: number; |
||||
|
||||
@TypeORM.PrimaryColumn({ type: "integer" }) |
||||
user_id: number; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
rating_after: number; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
rank: number; |
||||
|
||||
user: User; |
||||
|
||||
async loadRelationships() { |
||||
this.user = await User.findById(this.user_id); |
||||
} |
||||
} |
@ -0,0 +1,33 @@
|
||||
import * as TypeORM from "typeorm"; |
||||
import Model from "./common"; |
||||
|
||||
export enum StatisticsType { |
||||
FASTEST = "fastest", |
||||
SLOWEST = "slowest", |
||||
SHORTEST = "shortest", |
||||
LONGEST = "longest", |
||||
MEMORY_MIN = "min", |
||||
MEMORY_MAX = "max", |
||||
EARLIEST = "earliest" |
||||
} |
||||
|
||||
@TypeORM.Entity() |
||||
@TypeORM.Index(['problem_id', 'type', 'key']) |
||||
export default class SubmissionStatistics extends Model { |
||||
static cache = false; |
||||
|
||||
@TypeORM.PrimaryColumn({ type: "integer" }) |
||||
problem_id: number; |
||||
|
||||
@TypeORM.PrimaryColumn({ type: "integer" }) |
||||
user_id: number; |
||||
|
||||
@TypeORM.PrimaryColumn({ type: "enum", enum: StatisticsType }) |
||||
type: StatisticsType; |
||||
|
||||
@TypeORM.Column({ type: "integer" }) |
||||
key: number; |
||||
|
||||
@TypeORM.Column({ type: "integer" }) |
||||
submission_id: number; |
||||
}; |
@ -1,235 +0,0 @@
|
||||
let Sequelize = require('sequelize'); |
||||
let db = syzoj.db; |
||||
|
||||
let model = db.define('user', { |
||||
id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, |
||||
username: { type: Sequelize.STRING(80), unique: true }, |
||||
email: { type: Sequelize.STRING(120) }, |
||||
password: { type: Sequelize.STRING(120) }, |
||||
|
||||
nickname: { type: Sequelize.STRING(80) }, |
||||
nameplate: { type: Sequelize.TEXT }, |
||||
information: { type: Sequelize.TEXT }, |
||||
|
||||
ac_num: { type: Sequelize.INTEGER }, |
||||
submit_num: { type: Sequelize.INTEGER }, |
||||
|
||||
is_admin: { type: Sequelize.BOOLEAN }, |
||||
is_show: { type: Sequelize.BOOLEAN }, |
||||
public_email: { type: Sequelize.BOOLEAN }, |
||||
prefer_formatted_code: { type: Sequelize.BOOLEAN }, |
||||
|
||||
sex: { type: Sequelize.INTEGER }, |
||||
rating: { type: Sequelize.INTEGER }, |
||||
|
||||
register_time: { type: Sequelize.INTEGER } |
||||
}, { |
||||
timestamps: false, |
||||
tableName: 'user', |
||||
indexes: [ |
||||
{ |
||||
fields: ['username'], |
||||
unique: true |
||||
}, |
||||
{ |
||||
fields: ['nickname'], |
||||
}, |
||||
{ |
||||
fields: ['ac_num'], |
||||
} |
||||
] |
||||
}); |
||||
|
||||
let Model = require('./common'); |
||||
class User extends Model { |
||||
static async create(val) { |
||||
return User.fromRecord(User.model.build(Object.assign({ |
||||
username: '', |
||||
password: '', |
||||
email: '', |
||||
|
||||
nickname: '', |
||||
is_admin: false, |
||||
ac_num: 0, |
||||
submit_num: 0, |
||||
sex: 0, |
||||
is_show: syzoj.config.default.user.show, |
||||
rating: syzoj.config.default.user.rating, |
||||
register_time: parseInt((new Date()).getTime() / 1000), |
||||
prefer_formatted_code: true |
||||
}, val))); |
||||
} |
||||
|
||||
static async fromEmail(email) { |
||||
return User.fromRecord(User.model.findOne({ |
||||
where: { |
||||
email: email |
||||
} |
||||
})); |
||||
} |
||||
|
||||
static async fromName(name) { |
||||
return User.fromRecord(User.model.findOne({ |
||||
where: { |
||||
username: name |
||||
} |
||||
})); |
||||
} |
||||
|
||||
async isAllowedEditBy(user) { |
||||
if (!user) return false; |
||||
if (await user.hasPrivilege('manage_user')) return true; |
||||
return user && (user.is_admin || this.id === user.id); |
||||
} |
||||
|
||||
async refreshSubmitInfo() { |
||||
await syzoj.utils.lock(['User::refreshSubmitInfo', this.id], async () => { |
||||
let JudgeState = syzoj.model('judge_state'); |
||||
this.ac_num = await JudgeState.model.count({ |
||||
col: 'problem_id', |
||||
distinct: true, |
||||
where: { |
||||
user_id: this.id, |
||||
status: 'Accepted', |
||||
type: { |
||||
$ne: 1 // Not a contest submission
|
||||
} |
||||
} |
||||
}); |
||||
|
||||
this.submit_num = await JudgeState.count({ |
||||
user_id: this.id, |
||||
type: { |
||||
$ne: 1 // Not a contest submission
|
||||
} |
||||
}); |
||||
|
||||
await this.save(); |
||||
}); |
||||
} |
||||
|
||||
async getACProblems() { |
||||
let JudgeState = syzoj.model('judge_state'); |
||||
|
||||
let queryResult = await JudgeState.model.aggregate('problem_id', 'DISTINCT', { |
||||
plain: false, |
||||
where: { |
||||
user_id: this.id, |
||||
status: 'Accepted', |
||||
type: { |
||||
$ne: 1 // Not a contest submissio
|
||||
} |
||||
}, |
||||
order: [["problem_id", "ASC"]] |
||||
}); |
||||
|
||||
return queryResult.map(record => record['DISTINCT']) |
||||
} |
||||
|
||||
async getArticles() { |
||||
let Article = syzoj.model('article'); |
||||
|
||||
let all = await Article.model.findAll({ |
||||
attributes: ['id', 'title', 'public_time'], |
||||
where: { |
||||
user_id: this.id |
||||
} |
||||
}); |
||||
|
||||
return all.map(x => ({ |
||||
id: x.get('id'), |
||||
title: x.get('title'), |
||||
public_time: x.get('public_time') |
||||
})); |
||||
} |
||||
|
||||
async getStatistics() { |
||||
let JudgeState = syzoj.model('judge_state'); |
||||
|
||||
let statuses = { |
||||
"Accepted": ["Accepted"], |
||||
"Wrong Answer": ["Wrong Answer", "File Error", "Output Limit Exceeded"], |
||||
"Runtime Error": ["Runtime Error"], |
||||
"Time Limit Exceeded": ["Time Limit Exceeded"], |
||||
"Memory Limit Exceeded": ["Memory Limit Exceeded"], |
||||
"Compile Error": ["Compile Error"] |
||||
}; |
||||
|
||||
let res = {}; |
||||
for (let status in statuses) { |
||||
res[status] = 0; |
||||
for (let s of statuses[status]) { |
||||
res[status] += await JudgeState.count({ |
||||
user_id: this.id, |
||||
type: 0, |
||||
status: s |
||||
}); |
||||
} |
||||
} |
||||
|
||||
return res; |
||||
} |
||||
|
||||
async renderInformation() { |
||||
this.information = await syzoj.utils.markdown(this.information); |
||||
} |
||||
|
||||
async getPrivileges() { |
||||
let UserPrivilege = syzoj.model('user_privilege'); |
||||
let privileges = await UserPrivilege.query(null, { |
||||
user_id: this.id |
||||
}); |
||||
|
||||
return privileges.map(x => x.privilege); |
||||
} |
||||
|
||||
async setPrivileges(newPrivileges) { |
||||
let UserPrivilege = syzoj.model('user_privilege'); |
||||
|
||||
let oldPrivileges = await this.getPrivileges(); |
||||
|
||||
let delPrivileges = oldPrivileges.filter(x => !newPrivileges.includes(x)); |
||||
let addPrivileges = newPrivileges.filter(x => !oldPrivileges.includes(x)); |
||||
|
||||
for (let privilege of delPrivileges) { |
||||
let obj = await UserPrivilege.findOne({ where: { |
||||
user_id: this.id, |
||||
privilege: privilege |
||||
} }); |
||||
|
||||
await obj.destroy(); |
||||
} |
||||
|
||||
for (let privilege of addPrivileges) { |
||||
let obj = await UserPrivilege.create({ |
||||
user_id: this.id, |
||||
privilege: privilege |
||||
}); |
||||
|
||||
await obj.save(); |
||||
} |
||||
} |
||||
|
||||
async hasPrivilege(privilege) { |
||||
if (this.is_admin) return true; |
||||
|
||||
let UserPrivilege = syzoj.model('user_privilege'); |
||||
let x = await UserPrivilege.findOne({ where: { user_id: this.id, privilege: privilege } }); |
||||
return !(!x); |
||||
} |
||||
|
||||
async getLastSubmitLanguage() { |
||||
let JudgeState = syzoj.model('judge_state'); |
||||
|
||||
let a = await JudgeState.query([1, 1], { user_id: this.id }, [['submit_time', 'desc']]); |
||||
if (a[0]) return a[0].language; |
||||
|
||||
return null; |
||||
} |
||||
|
||||
getModel() { return model; } |
||||
} |
||||
|
||||
User.model = model; |
||||
|
||||
module.exports = User; |
@ -0,0 +1,207 @@
|
||||
import * as TypeORM from "typeorm"; |
||||
import Model from "./common"; |
||||
|
||||
declare var syzoj: any; |
||||
|
||||
import JudgeState from "./judge_state"; |
||||
import UserPrivilege from "./user_privilege"; |
||||
import Article from "./article"; |
||||
|
||||
@TypeORM.Entity() |
||||
export default class User extends Model { |
||||
static cache = true; |
||||
|
||||
@TypeORM.PrimaryGeneratedColumn() |
||||
id: number; |
||||
|
||||
@TypeORM.Index({ unique: true }) |
||||
@TypeORM.Column({ nullable: true, type: "varchar", length: 80 }) |
||||
username: string; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "varchar", length: 120 }) |
||||
email: string; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "varchar", length: 120 }) |
||||
password: string; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "varchar", length: 80 }) |
||||
nickname: string; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "text" }) |
||||
nameplate: string; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "text" }) |
||||
information: string; |
||||
|
||||
@TypeORM.Index() |
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
ac_num: number; |
||||
|
||||
@TypeORM.Index() |
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
submit_num: number; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "boolean" }) |
||||
is_admin: boolean; |
||||
|
||||
@TypeORM.Index() |
||||
@TypeORM.Column({ nullable: true, type: "boolean" }) |
||||
is_show: boolean; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "boolean", default: true }) |
||||
public_email: boolean; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "boolean", default: true }) |
||||
prefer_formatted_code: boolean; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
sex: number; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
rating: number; |
||||
|
||||
@TypeORM.Column({ nullable: true, type: "integer" }) |
||||
register_time: number; |
||||
|
||||
static async fromEmail(email): Promise<User> { |
||||
return User.findOne({ |
||||
where: { |
||||
email: email |
||||
} |
||||
}); |
||||
} |
||||
|
||||
static async fromName(name): Promise<User> { |
||||
return User.findOne({ |
||||
where: { |
||||
username: name |
||||
} |
||||
}); |
||||
} |
||||
|
||||
async isAllowedEditBy(user) { |
||||
if (!user) return false; |
||||
if (await user.hasPrivilege('manage_user')) return true; |
||||
return user && (user.is_admin || this.id === user.id); |
||||
} |
||||
|
||||
getQueryBuilderForACProblems() { |
||||
return JudgeState.createQueryBuilder() |
||||
.select(`DISTINCT(problem_id)`) |
||||
.where('user_id = :user_id', { user_id: this.id }) |
||||
.andWhere('status = :status', { status: 'Accepted' }) |
||||
.andWhere('type != 1') |
||||
.orderBy({ problem_id: 'ASC' }) |
||||
} |
||||
|
||||
async refreshSubmitInfo() { |
||||
await syzoj.utils.lock(['User::refreshSubmitInfo', this.id], async () => { |
||||
this.ac_num = await JudgeState.countQuery(this.getQueryBuilderForACProblems()); |
||||
this.submit_num = await JudgeState.count({ |
||||
user_id: this.id, |
||||
type: TypeORM.Not(1) // Not a contest submission
|
||||
}); |
||||
|
||||
await this.save(); |
||||
}); |
||||
} |
||||
|
||||
async getACProblems() { |
||||
let queryResult = await this.getQueryBuilderForACProblems().getRawMany(); |
||||
|
||||
return queryResult.map(record => record['problem_id']) |
||||
} |
||||
|
||||
async getArticles() { |
||||
return await Article.find({ |
||||
where: { |
||||
user_id: this.id |
||||
} |
||||
}); |
||||
} |
||||
|
||||
async getStatistics() { |
||||
let statuses = { |
||||
"Accepted": ["Accepted"], |
||||
"Wrong Answer": ["Wrong Answer", "File Error", "Output Limit Exceeded"], |
||||
"Runtime Error": ["Runtime Error"], |
||||
"Time Limit Exceeded": ["Time Limit Exceeded"], |
||||
"Memory Limit Exceeded": ["Memory Limit Exceeded"], |
||||
"Compile Error": ["Compile Error"] |
||||
}; |
||||
|
||||
let res = {}; |
||||
for (let status in statuses) { |
||||
res[status] = 0; |
||||
for (let s of statuses[status]) { |
||||
res[status] += await JudgeState.count({ |
||||
user_id: this.id, |
||||
type: 0, |
||||
status: s |
||||
}); |
||||
} |
||||
} |
||||
|
||||
return res; |
||||
} |
||||
|
||||
async renderInformation() { |
||||
this.information = await syzoj.utils.markdown(this.information); |
||||
} |
||||
|
||||
async getPrivileges() { |
||||
let privileges = await UserPrivilege.find({ |
||||
where: { |
||||
user_id: this.id |
||||
} |
||||
}); |
||||
|
||||
return privileges.map(x => x.privilege); |
||||
} |
||||
|
||||
async setPrivileges(newPrivileges) { |
||||
let oldPrivileges = await this.getPrivileges(); |
||||
|
||||
let delPrivileges = oldPrivileges.filter(x => !newPrivileges.includes(x)); |
||||
let addPrivileges = newPrivileges.filter(x => !oldPrivileges.includes(x)); |
||||
|
||||
for (let privilege of delPrivileges) { |
||||
let obj = await UserPrivilege.findOne({ where: { |
||||
user_id: this.id, |
||||
privilege: privilege |
||||
} }); |
||||
|
||||
await obj.destroy(); |
||||
} |
||||
|
||||
for (let privilege of addPrivileges) { |
||||
let obj = await UserPrivilege.create({ |
||||
user_id: this.id, |
||||
privilege: privilege |
||||
}); |
||||
|
||||
await obj.save(); |
||||
} |
||||
} |
||||
|
||||
async hasPrivilege(privilege) { |
||||
if (this.is_admin) return true; |
||||
|
||||
let x = await UserPrivilege.findOne({ where: { user_id: this.id, privilege: privilege } }); |
||||
return !!x; |
||||
} |
||||
|
||||
async getLastSubmitLanguage() { |
||||
let a = await JudgeState.findOne({ |
||||
where: { |
||||
user_id: this.id |
||||
}, |
||||
order: { |
||||
submit_time: 'DESC' |
||||
} |
||||
}); |
||||
if (a) return a.language; |
||||
|
||||
return null; |
||||
} |
||||
} |
@ -1,37 +0,0 @@
|
||||
let Sequelize = require('sequelize'); |
||||
let db = syzoj.db; |
||||
|
||||
let model = db.define('user_privilege', { |
||||
user_id: { type: Sequelize.INTEGER, primaryKey: true }, |
||||
privilege: { |
||||
type: Sequelize.STRING, |
||||
primaryKey: true |
||||
} |
||||
}, { |
||||
timestamps: false, |
||||
tableName: 'user_privilege', |
||||
indexes: [ |
||||
{ |
||||
fields: ['user_id'] |
||||
}, |
||||
{ |
||||
fields: ['privilege'] |
||||
} |
||||
] |
||||
}); |
||||
|
||||
let Model = require('./common'); |
||||
class UserPrivilege extends Model { |
||||
static async create(val) { |
||||
return UserPrivilege.fromRecord(UserPrivilege.model.build(Object.assign({ |
||||
user_id: 0, |
||||
privilege: '' |
||||
}, val))); |
||||
} |
||||
|
||||
getModel() { return model; } |
||||
} |
||||
|
||||
UserPrivilege.model = model; |
||||
|
||||
module.exports = UserPrivilege; |
@ -0,0 +1,13 @@
|
||||
import * as TypeORM from "typeorm"; |
||||
import Model from "./common"; |
||||
|
||||
@TypeORM.Entity() |
||||
export default class UserPrivilege extends Model { |
||||
@TypeORM.Index() |
||||
@TypeORM.PrimaryColumn({ type: "integer" }) |
||||
user_id: number; |
||||
|
||||
@TypeORM.Index() |
||||
@TypeORM.PrimaryColumn({ type: "varchar", length: 80 }) |
||||
privilege: string; |
||||
} |
@ -1,50 +0,0 @@
|
||||
let Sequelize = require('sequelize'); |
||||
let db = syzoj.db; |
||||
|
||||
let JudgeState = syzoj.model('judge_state'); |
||||
let CustomTest = syzoj.model('custom_test'); |
||||
|
||||
let model = db.define('waiting_judge', { |
||||
id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, |
||||
judge_id: { type: Sequelize.INTEGER }, |
||||
|
||||
// Smaller is higher
|
||||
priority: { type: Sequelize.INTEGER }, |
||||
|
||||
type: { |
||||
type: Sequelize.ENUM, |
||||
values: ['submission', 'custom-test'] |
||||
} |
||||
}, { |
||||
timestamps: false, |
||||
tableName: 'waiting_judge', |
||||
indexes: [ |
||||
{ |
||||
fields: ['judge_id'], |
||||
} |
||||
] |
||||
}); |
||||
|
||||
let Model = require('./common'); |
||||
class WaitingJudge extends Model { |
||||
static async create(val) { |
||||
return WaitingJudge.fromRecord(WaitingJudge.model.build(Object.assign({ |
||||
judge_id: 0, |
||||
priority: 0 |
||||
}, val))); |
||||
} |
||||
|
||||
async getCustomTest() { |
||||
return CustomTest.fromID(this.judge_id); |
||||
} |
||||
|
||||
async getJudgeState() { |
||||
return JudgeState.fromID(this.judge_id); |
||||
} |
||||
|
||||
getModel() { return model; } |
||||
} |
||||
|
||||
WaitingJudge.model = model; |
||||
|
||||
module.exports = WaitingJudge; |
@ -0,0 +1,11 @@
|
||||
{ |
||||
"compilerOptions": { |
||||
"module": "commonjs", |
||||
"preserveConstEnums": true, |
||||
"sourceMap": true, |
||||
"outDir": "./models-built", |
||||
"rootDir": "./models", |
||||
"emitDecoratorMetadata": true, |
||||
"experimentalDecorators": true |
||||
} |
||||
} |
@ -0,0 +1,23 @@
|
||||
<% if (paginate.hasPrevPage || paginate.hasNextPage) { %> |
||||
<% |
||||
const queryParams = Object.assign({}, req.query, { |
||||
currPageTop: paginate.top, |
||||
currPageBottom: paginate.bottom |
||||
}); |
||||
%> |
||||
<div style="text-align: center; "> |
||||
<div class="ui pagination menu" style="box-shadow: none; "> |
||||
<a class="<% if (!paginate.hasPrevPage) { %> disabled<% } %> icon item" |
||||
<% if (paginate.hasPrevPage) { %>href="<%= syzoj.utils.makeUrl(req, Object.assign({}, queryParams, { page: -1 })) %>" |
||||
<% } %>id="page_prev"> |
||||
<i class="left chevron icon"></i> |
||||
</a> |
||||
|
||||
<a class="<% if (!paginate.hasNextPage) { %> disabled<% } %> icon item" |
||||
<% if (paginate.hasNextPage) { %>href="<%= syzoj.utils.makeUrl(req, Object.assign({}, queryParams, { page: +1 })) %>" |
||||
<% } %>id="page_next"> |
||||
<i class="right chevron icon"></i> |
||||
</a> |
||||
</div> |
||||
</div> |
||||
<% } %> |
Loading…
Reference in new issue