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