Browse Source

Merge pull request #120 from syzoj/typeorm

Replace Sequelize ORM with TypeORM
pull/6/head
Menci 6 years ago committed by GitHub
parent
commit
07a5335bb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitignore
  2. 38
      README.en.md
  3. 39
      README.md
  4. 126
      app.js
  5. 5
      config-example.json
  6. 8
      libs/highlight.js
  7. 6
      libs/judger.js
  8. 33
      libs/markdown.js
  9. 32
      libs/renderer.js
  10. 61
      libs/rendererd.js
  11. 37
      migrates/build-statistics.js
  12. 47
      migrates/format-old-codes.js
  13. 127
      migrates/html-table-merge-cell-to-md.js
  14. 56
      models/article-comment.js
  15. 38
      models/article-comment.ts
  16. 91
      models/article.js
  17. 80
      models/article.ts
  18. 136
      models/common.js
  19. 208
      models/common.ts
  20. 144
      models/contest.ts
  21. 85
      models/contest_player.ts
  22. 48
      models/contest_ranklist.ts
  23. 81
      models/custom_test.js
  24. 71
      models/file.ts
  25. 31
      models/formatted_code.js
  26. 11
      models/formatted_code.ts
  27. 195
      models/judge_state.js
  28. 187
      models/judge_state.ts
  29. 683
      models/problem.js
  30. 595
      models/problem.ts
  31. 33
      models/problem_tag.js
  32. 17
      models/problem_tag.ts
  33. 37
      models/problem_tag_map.js
  34. 13
      models/problem_tag_map.ts
  35. 53
      models/rating_calculation.js
  36. 47
      models/rating_calculation.ts
  37. 43
      models/rating_history.js
  38. 27
      models/rating_history.ts
  39. 33
      models/submission_statistics.ts
  40. 235
      models/user.js
  41. 207
      models/user.ts
  42. 37
      models/user_privilege.js
  43. 13
      models/user_privilege.ts
  44. 50
      models/waiting_judge.js
  45. 117
      modules/admin.js
  46. 10
      modules/api.js
  47. 38
      modules/api_v2.js
  48. 120
      modules/contest.js
  49. 57
      modules/discussion.js
  50. 17
      modules/index.js
  51. 198
      modules/problem.js
  52. 4
      modules/problem_tag.js
  53. 113
      modules/submission.js
  54. 17
      modules/user.js
  55. 11
      package.json
  56. 11
      tsconfig.json
  57. 21
      utility.js
  58. 23
      views/page_fast.ejs
  59. 6
      views/submissions.ejs
  60. 638
      yarn.lock

1
.gitignore vendored

@ -3,6 +3,7 @@ config.json
*.db
.git.*
package-lock.json
models-built
# Logs
logs

38
README.en.md

@ -12,41 +12,5 @@ Currently, the tutorial for deploying is only available in Chinese. It's [部署
Join QQ group [565280992](https://jq.qq.com/?_wv=1027&k=5JQZWwd) or Telegram group [@lojdev](https://t.me/lojdev) for help.
# Upgrading
Because of updates to the database structure, users who upgrade from a commit BEFORE [d5bcbe8fb79e80f9d603b764ac787295cceffa34](https://github.com/syzoj/syzoj/commit/d5bcbe8fb79e80f9d603b764ac787295cceffa34) (Feb 21, 2018) **MUST** perform the following SQL on the database.
Currently, the tutorial for upgrading is only available in Chinese. It's [更新指南](https://github.com/syzoj/syzoj/wiki/%E6%9B%B4%E6%96%B0%E6%8C%87%E5%8D%97) in this project's wiki.
```sql
ALTER TABLE `judge_state` ADD `is_public` TINYINT(1) NOT NULL AFTER `compilation`;
UPDATE `judge_state` JOIN `problem` ON `problem`.`id` = `judge_state`.`problem_id` SET `judge_state`.`is_public` = `problem`.`is_public`;
ALTER TABLE `syzoj`.`judge_state` ADD INDEX `judge_state_is_public` (`id`, `is_public`, `type_info`, `type`);
```
Who upgrade from a commit BEFORE [26d66ceef24fbb35481317453bcb89ead6c69076](https://github.com/syzoj/syzoj/commit/26d66ceef24fbb35481317453bcb89ead6c69076) (Nov 5, 2018) **MUST** perform the following SQL on the database.
```sql
ALTER TABLE `contest_player` CHANGE `score_details` `score_details` JSON NOT NULL;
ALTER TABLE `contest_ranklist` CHANGE `ranking_params` `ranking_params` JSON NOT NULL;
ALTER TABLE `contest_ranklist` CHANGE `ranklist` `ranklist` JSON NOT NULL;
ALTER TABLE `custom_test` CHANGE `result` `result` JSON NOT NULL;
ALTER TABLE `judge_state` CHANGE `compilation` `compilation` JSON NOT NULL;
ALTER TABLE `judge_state` CHANGE `result` `result` JSON NOT NULL;
```
Who upgraded from a commit BEFORE [84b9e2d7b51e4ed3ab426621b66cf5ae9e1e1c23](https://github.com/syzoj/syzoj/commit/84b9e2d7b51e4ed3ab426621b66cf5ae9e1e1c23) (Nov 6, 2018) **MUST** perform the following SQL on the database.
```sql
ALTER TABLE `problem` ADD `publicize_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `type`;
```
Who upgraded from a commit BEFORE [d8be150fc6b8c43af61c5e4aca4fc0fe0445aef3](https://github.com/syzoj/syzoj/commit/d8be150fc6b8c43af61c5e4aca4fc0fe0445aef3) (Dec 7, 2018) **MUST** perform the following SQL on the database.
```sql
ALTER TABLE `user` ADD `prefer_formatted_code` TINYINT(1) NOT NULL DEFAULT 1 AFTER `public_email`;
```
To make code formatting work, `clang-format` needs to be installed. [migrates/format-old-codes.js](migrates/format-old-codes.js) may help formating old submissions' codes.
Who upgraded from a commit BEFORE [c192e8001ac81cab132ae033b39f09a094587633](https://github.com/syzoj/syzoj/commit/c192e8001ac81cab132ae033b39f09a094587633) (Mar 23, 2019) **MUST** install `redis-server` and [pygments](http://pygments.org/) on the web server. Markdown contents may be broken by switching to new renderer, [migrates/html-table-merge-cell-to-md.js](migrates/html-table-merge-cell-to-md.js) may help the migration。
Who upgraded from a commit BEFORE [7b03706821c604f59fe8263286203d57d634c421](https://github.com/syzoj/syzoj/commit/7b03706821c604f59fe8263286203d57d634c421) (Mar 27, 2019) **MUST** add `RemainAfterExit=yes` to the systemd config file `syzoj-web.service`'s `[Service]` section to make sure that restart service can work properly.
Who upgraded from a commit BEFORE [d1d019383e5cb0c96ed2191f900970654e4055c0](https://github.com/syzoj/syzoj/commit/d1d019383e5cb0c96ed2191f900970654e4055c0) (Mar 30, 2019) **MUST** upgrade web server's Redis to at least version 5, and fill web config's `judge_token` with random key. Judge server must be upgraded together, and `daemon-config.json`'s `ServerUrl` and `ServerToken` (= `judge_token`) must be filled. If judge and web run on different server, it's recommend to move RabbitMQ to judge server.

39
README.md

@ -12,41 +12,4 @@
加入 QQ 群 [565280992](https://jq.qq.com/?_wv=1027&k=5JQZWwd) 或 Telegram 群 [@lojdev](https://t.me/lojdev) 以取得帮助。
# 升级须知
因为一些数据库结构的更新,从该 commit [d5bcbe8fb79e80f9d603b764ac787295cceffa34](https://github.com/syzoj/syzoj/commit/d5bcbe8fb79e80f9d603b764ac787295cceffa34)(2018 年 4 月 21 日)前更新的用户**必须**在其数据库上执行以下 SQL 语句。
```sql
ALTER TABLE `judge_state` ADD `is_public` TINYINT(1) NOT NULL AFTER `compilation`;
UPDATE `judge_state` JOIN `problem` ON `problem`.`id` = `judge_state`.`problem_id` SET `judge_state`.`is_public` = `problem`.`is_public`;
ALTER TABLE `syzoj`.`judge_state` ADD INDEX `judge_state_is_public` (`id`, `is_public`, `type_info`, `type`);
```
从该 commit [26d66ceef24fbb35481317453bcb89ead6c69076](https://github.com/syzoj/syzoj/commit/26d66ceef24fbb35481317453bcb89ead6c69076)(2018 年 11 月 5 日)前更新的用户**必须**在其数据库上执行以下 SQL 语句。
```sql
ALTER TABLE `contest_player` CHANGE `score_details` `score_details` JSON NOT NULL;
ALTER TABLE `contest_ranklist` CHANGE `ranking_params` `ranking_params` JSON NOT NULL;
ALTER TABLE `contest_ranklist` CHANGE `ranklist` `ranklist` JSON NOT NULL;
ALTER TABLE `custom_test` CHANGE `result` `result` JSON NOT NULL;
ALTER TABLE `judge_state` CHANGE `compilation` `compilation` JSON NOT NULL;
ALTER TABLE `judge_state` CHANGE `result` `result` JSON NOT NULL;
```
从该 commit [84b9e2d7b51e4ed3ab426621b66cf5ae9e1e1c23](https://github.com/syzoj/syzoj/commit/84b9e2d7b51e4ed3ab426621b66cf5ae9e1e1c23)(2018 年 11 月 6 日)前更新的用户**必须**在其数据库上执行以下 SQL 语句。
```sql
ALTER TABLE `problem` ADD `publicize_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `type`;
```
从该 commit [d8be150fc6b8c43af61c5e4aca4fc0fe0445aef3](https://github.com/syzoj/syzoj/commit/d8be150fc6b8c43af61c5e4aca4fc0fe0445aef3)(2018 年 12 月 7 日)前更新的用户**必须**在其数据库上执行以下 SQL 语句。
```sql
ALTER TABLE `user` ADD `prefer_formatted_code` TINYINT(1) NOT NULL DEFAULT 1 AFTER `public_email`;
```
为使代码格式化功能正常工作,`clang-format` 需要被安装。[migrates/format-old-codes.js](migrates/format-old-codes.js) 可能对格式化旧提交记录的代码有帮助。
从该 commit [c192e8001ac81cab132ae033b39f09a094587633](https://github.com/syzoj/syzoj/commit/c192e8001ac81cab132ae033b39f09a094587633)(2019 年 3 月 23 日)前更新的用户**必须**在网站服务器上安装 `redis-server` 与 [pygments](http://pygments.org/)。旧的 Markdown 内容可能因切换到新渲染器被破坏,[migrates/html-table-merge-cell-to-md.js](migrates/html-table-merge-cell-to-md.js) 可能对迁移有所帮助。
从该 commit [7b03706821c604f59fe8263286203d57d634c421](https://github.com/syzoj/syzoj/commit/7b03706821c604f59fe8263286203d57d634c421)(2019 年 3 月 27 日)前更新的用户**必须**在其 systemd 配置文件 `syzoj-web.service` 中的 `[Service]` 中加入一行 `RemainAfterExit=yes`,以使得重启服务功能正常工作。
从该 commit [d1d019383e5cb0c96ed2191f900970654e4055c0](https://github.com/syzoj/syzoj/commit/d1d019383e5cb0c96ed2191f900970654e4055c0)(2019 年 3 月 30 日)前更新的用户**必须**将网站服务器上的 Redis 更新到 5 或更高版本,并填写网站配置中的 `judge_token` 为随机密钥。评测端需要被同步更新,并填写 `daemon-config.json` 中的 `ServerUrl``ServerToken`(需要与 `judge_token` 相同)。如果评测端与网站在不同的服务器上,建议迁移 RabbitMQ 到评测服务器上。
见本项目 Wiki 中的 [更新指南](https://github.com/syzoj/syzoj/wiki/%E6%9B%B4%E6%96%B0%E6%8C%87%E5%8D%97)。

126
app.js

@ -7,11 +7,22 @@ const fs = require('fs'),
commandLineArgs = require('command-line-args');
const optionDefinitions = [
{ name: 'config', alias: 'c', type: String, defaultValue: './config.json' },
{ name: 'config', alias: 'c', type: String, defaultValue: __dirname + '/config.json' },
];
const options = commandLineArgs(optionDefinitions);
require('reflect-metadata');
global.Promise = require('bluebird');
// Disable 'Warning: a promise was created in a handler at ...'
Promise.config({
warnings: {
wForgottenReturn: false
}
});
global.syzoj = {
rootDir: __dirname,
config: require('object-assign-deep')({}, require('./config-example.json'), require(options.config)),
@ -25,6 +36,16 @@ global.syzoj = {
if (obj instanceof ErrorMessage) return;
console.log(obj);
},
checkMigratedToTypeORM() {
const userConfig = require(options.config);
if (!userConfig.db.migrated_to_typeorm) {
app.use((req, res) => res.send('Please refer to <a href="https://github.com/syzoj/syzoj/wiki/TypeORM-%E8%BF%81%E7%A7%BB%E6%8C%87%E5%8D%97">TypeORM Migration Guide</a>.'));
app.listen(parseInt(syzoj.config.port), syzoj.config.hostname);
return false;
}
return true;
},
async run() {
// Check config
if (syzoj.config.session_secret === '@SESSION_SECRET@'
@ -38,10 +59,14 @@ global.syzoj = {
let Express = require('express');
global.app = Express();
if (!this.checkMigratedToTypeORM()) return;
syzoj.production = app.get('env') === 'production';
let winstonLib = require('./libs/winston');
winstonLib.configureWinston(!syzoj.production);
this.utils = require('./utility');
// Set assets dir
app.use(Express.static(__dirname + '/static', { maxAge: syzoj.production ? '1y' : 0 }));
@ -121,83 +146,55 @@ global.syzoj = {
});
},
async connectDatabase() {
let Sequelize = require('sequelize');
let Op = Sequelize.Op;
let operatorsAliases = {
$eq: Op.eq,
$ne: Op.ne,
$gte: Op.gte,
$gt: Op.gt,
$lte: Op.lte,
$lt: Op.lt,
$not: Op.not,
$in: Op.in,
$notIn: Op.notIn,
$is: Op.is,
$like: Op.like,
$notLike: Op.notLike,
$iLike: Op.iLike,
$notILike: Op.notILike,
$regexp: Op.regexp,
$notRegexp: Op.notRegexp,
$iRegexp: Op.iRegexp,
$notIRegexp: Op.notIRegexp,
$between: Op.between,
$notBetween: Op.notBetween,
$overlap: Op.overlap,
$contains: Op.contains,
$contained: Op.contained,
$adjacent: Op.adjacent,
$strictLeft: Op.strictLeft,
$strictRight: Op.strictRight,
$noExtendRight: Op.noExtendRight,
$noExtendLeft: Op.noExtendLeft,
$and: Op.and,
$or: Op.or,
$any: Op.any,
$all: Op.all,
$values: Op.values,
$col: Op.col
// Patch TypeORM to workaround https://github.com/typeorm/typeorm/issues/3636
const TypeORMMysqlDriver = require('typeorm/driver/mysql/MysqlDriver');
const OriginalNormalizeType = TypeORMMysqlDriver.MysqlDriver.prototype.normalizeType;
TypeORMMysqlDriver.MysqlDriver.prototype.normalizeType = function (column) {
if (column.type === 'json') {
return 'longtext';
}
return OriginalNormalizeType(column);
};
this.db = new Sequelize(this.config.db.database, this.config.db.username, this.config.db.password, {
host: this.config.db.host,
dialect: 'mariadb',
logging: syzoj.production ? false : syzoj.log,
timezone: require('moment')().format('Z'),
operatorsAliases: operatorsAliases
});
global.Promise = Sequelize.Promise;
this.db.countQuery = async (sql, options) => (await this.db.query(`SELECT COUNT(*) FROM (${sql}) AS \`__tmp_table\``, options))[0][0]['COUNT(*)'];
const TypeORM = require('typeorm');
global.TypeORM = TypeORM;
await this.loadModels();
},
loadModules() {
fs.readdir('./modules/', (err, files) => {
if (err) {
this.log(err);
return;
const modelsPath = __dirname + '/models/';
const modelsBuiltPath = __dirname + '/models-built/';
const models = fs.readdirSync(modelsPath)
.filter(filename => filename.endsWith('.ts') && filename !== 'common.ts')
.map(filename => require(modelsBuiltPath + filename.replace('.ts', '.js')).default);
await TypeORM.createConnection({
type: 'mariadb',
host: this.config.db.host.split(':')[0],
port: this.config.db.host.split(':')[1] || 3306,
username: this.config.db.username,
password: this.config.db.password,
database: this.config.db.database,
entities: models,
synchronize: true,
logging: !syzoj.production,
extra: {
connectionLimit: 50
}
files.filter((file) => file.endsWith('.js'))
.forEach((file) => this.modules.push(require(`./modules/${file}`)));
});
},
async loadModels() {
fs.readdir('./models/', (err, files) => {
loadModules() {
fs.readdir(__dirname + '/modules/', (err, files) => {
if (err) {
this.log(err);
return;
}
files.filter((file) => file.endsWith('.js'))
.forEach((file) => require(`./models/${file}`));
.forEach((file) => this.modules.push(require(`./modules/${file}`)));
});
await this.db.sync();
},
lib(name) {
return require(`./libs/${name}`);
},
model(name) {
return require(`./models/${name}`);
return require(`./models-built/${name}`).default;
},
loadHooks() {
let Session = require('express-session');
@ -221,7 +218,7 @@ global.syzoj = {
let User = syzoj.model('user');
if (req.session.user_id) {
User.fromID(req.session.user_id).then((user) => {
User.findById(req.session.user_id).then((user) => {
res.locals.user = user;
next();
}).catch((err) => {
@ -275,8 +272,7 @@ global.syzoj = {
res.locals.res = res;
next();
});
},
utils: require('./utility')
}
};
syzoj.run();
syzoj.untilStarted = syzoj.run();

5
config-example.json

@ -6,7 +6,9 @@
"database": "syzoj",
"username": "syzoj",
"password": "@DATABASE_PASSWORD@",
"host": "127.0.0.1"
"host": "127.0.0.1",
"migrated_to_typeorm": true,
"cache_size": 1000
},
"logo": {
"url": null,
@ -122,5 +124,6 @@
"url": "https://fonts.loli.net"
},
"no_cdn": false,
"submissions_page_fast_pagination": false,
"username_regex": "^[a-zA-Z0-9\\-\\_]+$"
}

8
libs/highlight.js

@ -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);
}

6
libs/judger.js

@ -2,7 +2,7 @@ const enums = require('./enums');
const util = require('util');
const winston = require('winston');
const msgPack = require('msgpack-lite');
const fs = Promise.promisifyAll(require('fs-extra'));
const fs = require('fs-extra');
const interface = require('./judger_interfaces');
const judgeResult = require('./judgeResult');
@ -179,7 +179,7 @@ async function connect() {
judge_state.max_memory = convertedResult.memory;
judge_state.result = convertedResult.result;
await judge_state.save();
await judge_state.updateRelatedInfo();
await judge_state.updateRelatedInfo(false);
} else if (result.type == interface.ProgressReportType.Compiled) {
if (!judge_state) return;
judge_state.compilation = result.progress;
@ -198,7 +198,7 @@ module.exports.judge = async function (judge_state, problem, priority) {
case 'submit-answer':
type = enums.ProblemType.AnswerSubmission;
param = null;
extraData = await fs.readFileAsync(syzoj.model('file').resolvePath('answer', judge_state.code));
extraData = await fs.readFile(syzoj.model('file').resolvePath('answer', judge_state.code));
break;
case 'interaction':
type = enums.ProblemType.Interaction;

33
libs/markdown.js

@ -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);
};

32
libs/renderer.js

@ -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
}
});
}

61
libs/rendererd.js

@ -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());

37
migrates/build-statistics.js

@ -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();

47
migrates/format-old-codes.js

@ -1,47 +1,6 @@
/*
* This script will help format all submissions' codes whose id in a interval.
* After moving to TypeORM, this script no longer works.
* If you have NOT migrated to TypeORM, please follow
* https://github.com/syzoj/syzoj/wiki/TypeORM-%E8%BF%81%E7%A7%BB%E6%8C%87%E5%8D%97
*
* Useful when upgrading from a version that doesn't support code formatting.
*/
const JudgeState = syzoj.model('judge_state');
const FormattedCode = syzoj.model('formatted_code');
const CodeFormatter = syzoj.lib('code_formatter');
require('.');
const fn = async (begin, end) => {
for (let i = begin; i < end; i++) {
const judge_state = await JudgeState.fromID(i);
if (!judge_state) continue;
if (!judge_state.language) continue;
const key = syzoj.utils.getFormattedCodeKey(judge_state.code, judge_state.language);
if (!key) continue;
let formatted_code = await FormattedCode.findOne({ where: { key: key } });
const code = await CodeFormatter(judge_state.code, syzoj.languages[judge_state.language].format);
if (code === null) {
console.error(`Format ${i} failed.`);
continue;
}
if (!formatted_code) {
formatted_code = await FormattedCode.create({
key: key,
code: code
});
} else continue; // formatted_code.code = code;
try {
await formatted_code.save();
console.error(`Format and save ${i} success.`);
} catch (e) {
console.error(`Save ${i} failed:`, e);
}
}
};
// NOTE: Uncomment and fill arguments to run.
// fn(begin, end) // [begin, end)

127
migrates/html-table-merge-cell-to-md.js

@ -1,127 +1,6 @@
/*
* This script will help migrate from the old marked-based markdown renderer
* to the new markdown-it based (syzoj-renderer).
* After moving to TypeORM, this script no longer works.
* If you have NOT migrated to TypeORM, please follow
* https://github.com/syzoj/syzoj/wiki/TypeORM-%E8%BF%81%E7%A7%BB%E6%8C%87%E5%8D%97
*
* The later doesn't support inline markdown inside HTML blocks. But in
* LibreOJ that's widely used in problem's limits' cell-merged table displaying
* displaying. So TeX maths inside are broken.
*/
const cheerio = require('cheerio');
function processMarkdown(text) {
return text.replace(/(<table(?:[\S\s]+?)<\/table>)/gi, (match, offset, string) => {
const $ = cheerio.load(match, { decodeEntities: false }),
table = $('table');
let defaultAlign = '-';
if (table.hasClass('center')) defaultAlign = ':-:';
else if (table.hasClass('left')) defaultAlign = ':-';
else if (table.hasClass('right')) defaultAlign = '-:';
let columnCount = 0;
const columnAlign = [];
table.find('th').each((i, th) => {
const count = parseInt($(th).attr('colspan')) || 1;
columnCount += count;
const style = ($(th).attr('style') || '').split(' ').join('').toLowerCase();
if (style.includes('text-align:center')) columnAlign.push(':-:');
else if (style.includes('text-align:left')) columnAlign.push(':-');
else if (style.includes('text-align:right')) columnAlign.push('-:');
else columnAlign.push(defaultAlign);
});
const rowCount = table.find('tr').length;
function escape(s) {
return ` ${s.trim().split('|').join('\\|')} `;
}
const matrix = Array(rowCount).fill(null).map(() => []);
table.find('tr').each((i, tr) => {
const cells = $(tr).find('th, td');
let columnIndex = 0, resColumnIndex = 0;
cells.each((j, td) => {
while (typeof matrix[i][resColumnIndex] !== 'undefined') resColumnIndex++;
if (columnIndex >= columnCount) return false;
if (resColumnIndex >= columnCount) return false;
const colspan = parseInt($(td).attr('colspan')) || 1,
rowspan = parseInt($(td).attr('rowspan')) || 1,
content = $(td).html();
for (let cntRow = 0; cntRow < rowspan; cntRow++) {
for (let cntCol = 0; cntCol < colspan; cntCol++) {
if (i + cntRow < rowCount && resColumnIndex + cntCol < columnCount) {
matrix[i + cntRow][resColumnIndex + cntCol] = escape(content);
}
}
}
resColumnIndex += colspan;
});
});
const code = [matrix[0], columnAlign, ...matrix.slice(1)].map(row => `|${row.join('|')}|`).join('\n');
return `<!-- BEGIN: Migrated markdown table -->\n\n${code}\n\n<!-- Migrated from original HTML table:\n${match}\n-->\n\n<!-- END: Migrated markdown table -->`;
});
}
// Load syzoj.
process.chdir(__dirname + '/..');
require('..');
const modelFields = {
problem: [
'description',
'input_format',
'output_format',
'example',
'limit_and_hint'
],
contest: [
'information',
'problems'
],
article: [
'content'
],
'article-comment': [
'content'
]
};
const fn = async () => {
for (const model in modelFields) {
const modelObject = syzoj.model(model);
const allData = await modelObject.all();
let cnt = 0, tot = allData.length;
for (const obj of allData) {
console.log(`${model}: ${++cnt}/${tot}`);
let modified = false;
for (field of modelFields[model]) {
const processed = processMarkdown(obj[field]);
if (processed != obj[field]) {
obj[field] = processed;
modified = true;
}
}
if (modified) {
await obj.save();
}
}
}
process.exit();
};
// NOTE: Uncomment to run.
// fn();

56
models/article-comment.js

@ -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;

38
models/article-comment.ts

@ -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);
}
};

91
models/article.js

@ -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;

80
models/article.ts

@ -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();
});
}
};

136
models/common.js

@ -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;

208
models/common.ts

@ -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();
}
}

144
models/contest.js → models/contest.ts

@ -1,77 +1,71 @@
let Sequelize = require('sequelize');
let db = syzoj.db;
let User = syzoj.model('user');
let Problem = syzoj.model('problem');
let ContestRanklist = syzoj.model('contest_ranklist');
let ContestPlayer = syzoj.model('contest_player');
let model = db.define('contest', {
id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true },
title: { type: Sequelize.STRING(80) },
subtitle: { type: Sequelize.TEXT },
start_time: { type: Sequelize.INTEGER },
end_time: { type: Sequelize.INTEGER },
holder_id: {
type: Sequelize.INTEGER,
references: {
model: 'user',
key: 'id'
}
},
import * as TypeORM from "typeorm";
import Model from "./common";
declare var syzoj, ErrorMessage: any;
import User from "./user";
import Problem from "./problem";
import ContestRanklist from "./contest_ranklist";
import ContestPlayer from "./contest_player";
enum ContestType {
NOI = "noi",
IOI = "ioi",
ICPC = "acm"
}
@TypeORM.Entity()
export default class Contest extends Model {
static cache = true;
@TypeORM.PrimaryGeneratedColumn()
id: number;
@TypeORM.Column({ nullable: true, type: "varchar", length: 80 })
title: string;
@TypeORM.Column({ nullable: true, type: "text" })
subtitle: string;
@TypeORM.Column({ nullable: true, type: "integer" })
start_time: number;
@TypeORM.Column({ nullable: true, type: "integer" })
end_time: number;
@TypeORM.Index()
@TypeORM.Column({ nullable: true, type: "integer" })
holder_id: number;
// type: noi, ioi, acm
type: { type: Sequelize.STRING(10) },
@TypeORM.Column({ nullable: true, type: "enum", enum: ContestType })
type: ContestType;
information: { type: Sequelize.TEXT },
problems: { type: Sequelize.TEXT },
admins: { type: Sequelize.TEXT },
@TypeORM.Column({ nullable: true, type: "text" })
information: string;
ranklist_id: {
type: Sequelize.INTEGER,
references: {
model: 'contest_ranklist',
key: 'id'
}
},
is_public: { type: Sequelize.BOOLEAN },
hide_statistics: { type: Sequelize.BOOLEAN }
}, {
timestamps: false,
tableName: 'contest',
indexes: [
{
fields: ['holder_id'],
},
{
fields: ['ranklist_id'],
}
]
});
let Model = require('./common');
class Contest extends Model {
static async create(val) {
return Contest.fromRecord(Contest.model.build(Object.assign({
title: '',
subtitle: '',
problems: '',
admins: '',
information: '',
type: 'noi',
start_time: 0,
end_time: 0,
holder: 0,
ranklist_id: 0,
is_public: false,
hide_statistics: false
}, val)));
}
@TypeORM.Column({ nullable: true, type: "text" })
problems: string;
@TypeORM.Column({ nullable: true, type: "text" })
admins: string;
@TypeORM.Index()
@TypeORM.Column({ nullable: true, type: "integer" })
ranklist_id: number;
@TypeORM.Column({ nullable: true, type: "boolean" })
is_public: boolean;
@TypeORM.Column({ nullable: true, type: "boolean" })
hide_statistics: boolean;
holder?: User;
ranklist?: ContestRanklist;
async loadRelationships() {
this.holder = await User.fromID(this.holder_id);
this.ranklist = await ContestRanklist.fromID(this.ranklist_id);
this.holder = await User.findById(this.holder_id);
this.ranklist = await ContestRanklist.findById(this.ranklist_id);
}
async isSupervisior(user) {
@ -110,7 +104,7 @@ class Contest extends Model {
async setProblems(s) {
let a = [];
await s.split('|').forEachAsync(async x => {
let problem = await Problem.fromID(x);
let problem = await Problem.findById(x);
if (!problem) return;
a.push(x);
});
@ -146,19 +140,13 @@ class Contest extends Model {
});
}
isRunning(now) {
isRunning(now?) {
if (!now) now = syzoj.utils.getCurrentDate();
return now >= this.start_time && now < this.end_time;
}
isEnded(now) {
isEnded(now?) {
if (!now) now = syzoj.utils.getCurrentDate();
return now >= this.end_time;
}
getModel() { return model; }
}
Contest.model = model;
module.exports = Contest;

85
models/contest_player.js → models/contest_player.ts

@ -1,50 +1,43 @@
let Sequelize = require('sequelize');
let db = syzoj.db;
let User = syzoj.model('user');
let Problem = syzoj.model('problem');
let model = db.define('contest_player', {
id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true },
contest_id: { type: Sequelize.INTEGER },
user_id: { type: Sequelize.INTEGER },
score: { type: Sequelize.INTEGER },
score_details: { type: Sequelize.JSON },
time_spent: { type: Sequelize.INTEGER }
}, {
timestamps: false,
tableName: 'contest_player',
indexes: [
{
fields: ['contest_id'],
},
{
fields: ['user_id'],
}
]
});
let Model = require('./common');
class ContestPlayer extends Model {
static async create(val) {
return ContestPlayer.fromRecord(ContestPlayer.model.build(Object.assign({
contest_id: 0,
user_id: 0,
score: 0,
score_details: {},
time_spent: 0
}, val)));
}
import * as TypeORM from "typeorm";
import Model from "./common";
import User from "./user";
import Contest from "./contest";
@TypeORM.Entity()
export default class ContestPlayer extends Model {
static cache = true;
@TypeORM.PrimaryGeneratedColumn()
id: number;
@TypeORM.Index()
@TypeORM.Column({ nullable: true, type: "integer" })
contest_id: number;
@TypeORM.Index()
@TypeORM.Column({ nullable: true, type: "integer" })
user_id: number;
@TypeORM.Column({ nullable: true, type: "integer" })
score: number;
@TypeORM.Column({ nullable: true, type: "json" })
score_details: object;
@TypeORM.Column({ nullable: true, type: "integer" })
time_spent: number;
user?: User;
contest?: Contest;
static async findInContest(where) {
return ContestPlayer.findOne({ where: where });
}
async loadRelationships() {
let Contest = syzoj.model('contest');
this.user = await User.fromID(this.user_id);
this.contest = await Contest.fromID(this.contest_id);
this.user = await User.findById(this.user_id);
this.contest = await Contest.findById(this.contest_id);
}
async updateScore(judge_state) {
@ -65,7 +58,7 @@ class ContestPlayer extends Model {
time: judge_state.submit_time
};
let arr = Object.values(this.score_details[judge_state.problem_id].submissions);
let arr: any = Object.values(this.score_details[judge_state.problem_id].submissions);
arr.sort((a, b) => a.time - b.time);
let maxScoreSubmission = null;
@ -117,7 +110,7 @@ class ContestPlayer extends Model {
time: judge_state.submit_time
};
let arr = Object.values(this.score_details[judge_state.problem_id].submissions);
let arr: any = Object.values(this.score_details[judge_state.problem_id].submissions);
arr.sort((a, b) => a.time - b.time);
this.score_details[judge_state.problem_id].unacceptedCount = 0;
@ -145,10 +138,4 @@ class ContestPlayer extends Model {
}
}
}
getModel() { return model; }
}
ContestPlayer.model = model;
module.exports = ContestPlayer;

48
models/contest_ranklist.js → models/contest_ranklist.ts

@ -1,32 +1,26 @@
let Sequelize = require('sequelize');
let db = syzoj.db;
import * as TypeORM from "typeorm";
import Model from "./common";
let User = syzoj.model('user');
let Problem = syzoj.model('problem');
let ContestPlayer = syzoj.model('contest_player');
declare var syzoj: any;
let model = db.define('contest_ranklist', {
id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true },
ranking_params: { type: Sequelize.JSON },
ranklist: { type: Sequelize.JSON }
}, {
timestamps: false,
tableName: 'contest_ranklist'
});
import ContestPlayer from "./contest_player";
import JudgeState from "./judge_state";
let Model = require('./common');
class ContestRanklist extends Model {
static async create(val) {
return ContestRanklist.fromRecord(ContestRanklist.model.build(Object.assign({
ranking_params: {},
ranklist: {}
}, val)));
}
@TypeORM.Entity()
export default class ContestRanklist extends Model {
@TypeORM.PrimaryGeneratedColumn()
id: number;
@TypeORM.Column({ nullable: true, type: "json" })
ranking_params: any;
@TypeORM.Column({ nullable: true, type: "json" })
ranklist: any;
async getPlayers() {
let a = [];
for (let i = 1; i <= this.ranklist.player_num; i++) {
a.push(await ContestPlayer.fromID(this.ranklist[i]));
a.push(await ContestPlayer.findById(this.ranklist[i]));
}
return a;
}
@ -44,15 +38,13 @@ class ContestRanklist extends Model {
players.push(player);
}
let JudgeState = syzoj.model('judge_state');
if (contest.type === 'noi' || contest.type === 'ioi') {
for (let player of players) {
player.latest = 0;
player.score = 0;
for (let i in player.score_details) {
let judge_state = await JudgeState.fromID(player.score_details[i].judge_id);
let judge_state = await JudgeState.findById(player.score_details[i].judge_id);
if (!judge_state) continue;
player.latest = Math.max(player.latest, judge_state.submit_time);
@ -94,10 +86,4 @@ class ContestRanklist extends Model {
this.ranklist = { player_num: players.length };
for (let i = 0; i < players.length; i++) this.ranklist[i + 1] = players[i].id;
}
getModel() { return model; }
}
ContestRanklist.model = model;
module.exports = ContestRanklist;

81
models/custom_test.js

@ -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;

71
models/file.js → models/file.ts

@ -1,31 +1,24 @@
let Sequelize = require('sequelize');
let db = syzoj.db;
let model = db.define('file', {
id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true },
type: { type: Sequelize.STRING(80) },
md5: { type: Sequelize.STRING(80), unique: true }
}, {
timestamps: false,
tableName: 'file',
indexes: [
{
fields: ['type'],
},
{
fields: ['md5'],
}
]
});
let Model = require('./common');
class File extends Model {
static create(val) {
return File.fromRecord(File.model.build(Object.assign({
type: '',
md5: ''
}, val)));
}
import * as TypeORM from "typeorm";
import Model from "./common";
import * as fs from "fs-extra";
declare var syzoj, ErrorMessage: any;
@TypeORM.Entity()
export default class File extends Model {
@TypeORM.PrimaryGeneratedColumn()
id: number;
@TypeORM.Index()
@TypeORM.Column({ nullable: true, type: "varchar", length: 80 })
type: string;
@TypeORM.Index({ unique: true })
@TypeORM.Column({ nullable: true, type: "varchar", length: 80 })
md5: string;
unzipSize?: number;
getPath() {
return File.resolvePath(this.type, this.md5);
@ -54,24 +47,12 @@ class File extends Model {
}
static async upload(path, type, noLimit) {
let fs = Promise.promisifyAll(require('fs-extra'));
let buf = await fs.readFileAsync(path);
let buf = await fs.readFile(path);
if (!noLimit && buf.length > syzoj.config.limit.data_size) throw new ErrorMessage('数据包太大。');
try {
let p7zip = new (require('node-7z'));
this.unzipSize = 0;
await p7zip.list(path).progress(files => {
for (let file of files) this.unzipSize += file.size;
});
} catch (e) {
this.unzipSize = null;
}
let key = syzoj.utils.md5(buf);
await fs.moveAsync(path, File.resolvePath(type, key), { overwrite: true });
await fs.move(path, File.resolvePath(type, key), { overwrite: true });
let file = await File.findOne({ where: { md5: key } });
if (!file) {
@ -101,10 +82,4 @@ class File extends Model {
if (this.unzipSize === null) throw new ErrorMessage('无效的 ZIP 文件。');
else return this.unzipSize;
}
getModel() { return model; }
}
File.model = model;
module.exports = File;

31
models/formatted_code.js

@ -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;

11
models/formatted_code.ts

@ -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;
}

195
models/judge_state.js

@ -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;

187
models/judge_state.ts

@ -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;
}
}

683
models/problem.js

@ -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;

595
models/problem.ts

@ -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();
}
}

33
models/problem_tag.js

@ -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;

17
models/problem_tag.ts

@ -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;
}

37
models/problem_tag_map.js

@ -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;

13
models/problem_tag_map.ts

@ -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;
}

53
models/rating_calculation.js

@ -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;

47
models/rating_calculation.ts

@ -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();
}
}

43
models/rating_history.js

@ -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;

27
models/rating_history.ts

@ -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);
}
}

33
models/submission_statistics.ts

@ -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;
};

235
models/user.js

@ -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;

207
models/user.ts

@ -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;
}
}

37
models/user_privilege.js

@ -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;

13
models/user_privilege.ts

@ -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;
}

50
models/waiting_judge.js

@ -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;

117
modules/admin.js

@ -9,14 +9,14 @@ const RatingHistory = syzoj.model('rating_history');
let ContestPlayer = syzoj.model('contest_player');
const calcRating = require('../libs/rating');
let db = syzoj.db;
app.get('/admin/info', async (req, res) => {
try {
if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。');
let allSubmissionsCount = await JudgeState.count();
let todaySubmissionsCount = await JudgeState.count({ submit_time: { $gte: syzoj.utils.getCurrentDate(true) } });
let todaySubmissionsCount = await JudgeState.count({
submit_time: TypeORM.MoreThanOrEqual(syzoj.utils.getCurrentDate(true))
});
let problemsCount = await Problem.count();
let articlesCount = await Article.count();
let contestsCount = await Contest.count();
@ -119,12 +119,12 @@ app.get('/admin/privilege', async (req, res) => {
try {
if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。');
let a = await UserPrivilege.query();
let a = await UserPrivilege.find();
let users = {};
for (let p of a) {
if (!users[p.user_id]) {
users[p.user_id] = {
user: await User.fromID(p.user_id),
user: await User.findById(p.user_id),
privileges: []
};
}
@ -149,7 +149,7 @@ app.post('/admin/privilege', async (req, res) => {
let data = JSON.parse(req.body.data);
for (let id in data) {
let user = await User.fromID(id);
let user = await User.findById(id);
if (!user) throw new ErrorMessage(`不存在 ID 为 ${id} 的用户。`);
await user.setPrivileges(data[id]);
}
@ -166,9 +166,16 @@ app.post('/admin/privilege', async (req, res) => {
app.get('/admin/rating', async (req, res) => {
try {
if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。');
const contests = await Contest.query(null, {}, [['start_time', 'desc']]);
const calcs = await RatingCalculation.query(null, {}, [['id', 'desc']]);
const util = require('util');
const contests = await Contest.find({
order: {
start_time: 'DESC'
}
});
const calcs = await RatingCalculation.find({
order: {
id: 'DESC'
}
});
for (const calc of calcs) await calc.loadRelationships();
res.render('admin_rating', {
@ -186,7 +193,7 @@ app.get('/admin/rating', async (req, res) => {
app.post('/admin/rating/add', async (req, res) => {
try {
if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。');
const contest = await Contest.fromID(req.body.contest);
const contest = await Contest.findById(req.body.contest);
if (!contest) throw new ErrorMessage('无此比赛');
await contest.loadRelationships();
@ -199,7 +206,7 @@ app.post('/admin/rating/add', async (req, res) => {
const players = [];
for (let i = 1; i <= contest.ranklist.ranklist.player_num; i++) {
const user = await User.fromID((await ContestPlayer.fromID(contest.ranklist.ranklist[i])).user_id);
const user = await User.findById((await ContestPlayer.findById(contest.ranklist.ranklist[i])).user_id);
players.push({
user: user,
rank: i,
@ -227,7 +234,14 @@ app.post('/admin/rating/add', async (req, res) => {
app.post('/admin/rating/delete', async (req, res) => {
try {
if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。');
const calcList = await RatingCalculation.query(null, { id: { $gte: req.body.calc_id } }, [['id', 'desc']]);
const calcList = await RatingCalculation.find({
where: {
id: TypeORM.MoreThanOrEqual(req.body.calc_id)
},
order: {
id: 'DESC'
}
});
if (calcList.length === 0) throw new ErrorMessage('ID 不正确');
for (let i = 0; i < calcList.length; i++) {
@ -277,20 +291,20 @@ app.post('/admin/other', async (req, res) => {
if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。');
if (req.body.type === 'reset_count') {
const problems = await Problem.query();
const problems = await Problem.find();
for (const p of problems) {
await p.resetSubmissionCount();
}
} else if (req.body.type === 'reset_discussion') {
const articles = await Article.query();
const articles = await Article.find();
for (const a of articles) {
await a.resetReplyCountAndTime();
}
} else if (req.body.type === 'reset_codelen') {
const submissions = await JudgeState.query();
const submissions = await JudgeState.find();
for (const s of submissions) {
if (s.type !== 'submit-answer') {
s.code_length = s.code.length;
s.code_length = Buffer.from(s.code).length;
await s.save();
}
}
@ -310,60 +324,55 @@ app.post('/admin/rejudge', async (req, res) => {
try {
if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。');
let query = JudgeState.createQueryBuilder();
let user = await User.fromName(req.body.submitter || '');
let where = {};
if (user) where.user_id = user.id;
else if (req.body.submitter) where.user_id = -1;
if (user) {
query.andWhere('user_id = :user_id', { user_id: user.id });
} else if (req.body.submitter) {
query.andWhere('user_id = :user_id', { user_id: 0 });
}
let minID = parseInt(req.body.min_id);
if (isNaN(minID)) minID = 0;
if (!isNaN(minID)) query.andWhere('id >= :minID', { minID })
let maxID = parseInt(req.body.max_id);
if (isNaN(maxID)) maxID = 2147483647;
where.id = {
$and: {
$gte: parseInt(minID),
$lte: parseInt(maxID)
}
};
if (!isNaN(maxID)) query.andWhere('id <= :maxID', { maxID })
let minScore = parseInt(req.body.min_score);
if (isNaN(minScore)) minScore = 0;
if (!isNaN(minScore)) query.andWhere('score >= :minScore', { minScore });
let maxScore = parseInt(req.body.max_score);
if (isNaN(maxScore)) maxScore = 100;
if (!(minScore === 0 && maxScore === 100)) {
where.score = {
$and: {
$gte: parseInt(minScore),
$lte: parseInt(maxScore)
}
};
}
if (!isNaN(maxScore)) query.andWhere('score <= :maxScore', { maxScore });
let minTime = syzoj.utils.parseDate(req.body.min_time);
if (isNaN(minTime)) minTime = 0;
if (!isNaN(minTime)) query.andWhere('submit_time >= :minTime', { minTime: parseInt(minTime) });
let maxTime = syzoj.utils.parseDate(req.body.max_time);
if (isNaN(maxTime)) maxTime = 2147483647;
if (!isNaN(maxTime)) query.andWhere('submit_time <= :maxTime', { maxTime: parseInt(maxTime) });
where.submit_time = {
$and: {
$gte: parseInt(minTime),
$lte: parseInt(maxTime)
if (req.body.language) {
if (req.body.language === 'submit-answer') {
query.andWhere(new TypeORM.Brackets(qb => {
qb.orWhere('language = :language', { language: '' })
.orWhere('language IS NULL');
}));
} else if (req.body.language === 'non-submit-answer') {
query.andWhere('language != :language', { language: '' })
.andWhere('language IS NOT NULL');;
} else {
query.andWhere('language = :language', { language: req.body.language });
}
};
}
if (req.body.language) {
if (req.body.language === 'submit-answer') where.language = { $or: [{ $eq: '', }, { $eq: null }] };
else if (req.body.language === 'non-submit-answer') where.language = { $not: '' };
else where.language = req.body.language;
if (req.body.status) {
query.andWhere('status = :status', { status: req.body.status });
}
if (req.body.problem_id) {
query.andWhere('problem_id = :problem_id', { problem_id: parseInt(req.body.problem_id) || 0 })
}
if (req.body.status) where.status = { $like: req.body.status + '%' };
if (req.body.problem_id) where.problem_id = parseInt(req.body.problem_id) || -1;
let count = await JudgeState.count(where);
let count = await JudgeState.countQuery(query);
if (req.body.type === 'rejudge') {
let submissions = await JudgeState.query(null, where);
let submissions = await JudgeState.queryAll(query);
for (let submission of submissions) {
await submission.rejudge();
}

10
modules/api.js

@ -113,7 +113,9 @@ app.post('/api/sign_up', async (req, res) => {
username: req.body.username,
password: req.body.password,
email: req.body.email,
public_email: true
is_show: syzoj.config.default.user.show,
rating: syzoj.config.default.user.rating,
register_time: parseInt((new Date()).getTime() / 1000)
});
await user.save();
@ -158,7 +160,7 @@ app.post('/api/reset_password', async (req, res) => {
let syzoj2_xxx_md5 = '59cb65ba6f9ad18de0dcd12d5ae11bd2';
if (req.body.password === syzoj2_xxx_md5) throw new ErrorMessage('密码不能为空。');
const user = await User.fromID(obj.userId);
const user = await User.findById(obj.userId);
user.password = req.body.password;
await user.save();
@ -198,7 +200,9 @@ app.get('/api/sign_up_confirm', async (req, res) => {
username: obj.username,
password: obj.password,
email: obj.email,
public_email: true
is_show: syzoj.config.default.user.show,
rating: syzoj.config.default.user.rating,
register_time: parseInt((new Date()).getTime() / 1000)
});
await user.save();

38
modules/api_v2.js

@ -4,19 +4,23 @@ app.get('/api/v2/search/users/:keyword*?', async (req, res) => {
let keyword = req.params.keyword || '';
let conditions = [];
const uid = parseInt(keyword);
const uid = parseInt(keyword) || 0;
if (uid != null && !isNaN(uid)) {
conditions.push({ id: uid });
}
if (keyword != null && String(keyword).length >= 2) {
conditions.push({ username: { $like: `%${req.params.keyword}%` } });
conditions.push({ username: TypeORM.Like(`%${req.params.keyword}%`) });
}
if (conditions.length === 0) {
res.send({ success: true, results: [] });
} else {
let users = await User.query(null, {
$or: conditions
}, [['username', 'asc']]);
let users = await User.find({
where: conditions,
order: {
username: 'ASC'
}
});
let result = [];
@ -34,15 +38,20 @@ app.get('/api/v2/search/problems/:keyword*?', async (req, res) => {
let Problem = syzoj.model('problem');
let keyword = req.params.keyword || '';
let problems = await Problem.query(null, {
title: { $like: `%${req.params.keyword}%` }
}, [['id', 'asc']]);
let problems = await Problem.find({
where: {
title: TypeORM.Like(`%${req.params.keyword}%`)
},
order: {
id: 'ASC'
}
});
let result = [];
let id = parseInt(keyword);
if (id) {
let problemById = await Problem.fromID(parseInt(keyword));
let problemById = await Problem.findById(parseInt(keyword));
if (problemById && await problemById.isAllowedUseBy(res.locals.user)) {
result.push(problemById);
}
@ -67,9 +76,14 @@ app.get('/api/v2/search/tags/:keyword*?', async (req, res) => {
let ProblemTag = syzoj.model('problem_tag');
let keyword = req.params.keyword || '';
let tags = await ProblemTag.query(null, {
name: { $like: `%${req.params.keyword}%` }
}, [['name', 'asc']]);
let tags = await ProblemTag.find({
where: {
name: TypeORM.Like(`%${req.params.keyword}%`)
},
order: {
name: 'ASC'
}
});
let result = tags.slice(0, syzoj.config.page.edit_problem_tag_list);

120
modules/contest.js

@ -14,8 +14,10 @@ app.get('/contests', async (req, res) => {
if (res.locals.user && res.locals.user.is_admin) where = {}
else where = { is_public: true };
let paginate = syzoj.utils.paginate(await Contest.count(where), req.query.page, syzoj.config.page.contest);
let contests = await Contest.query(paginate, where, [['start_time', 'desc']]);
let paginate = syzoj.utils.paginate(await Contest.countForPagination(where), req.query.page, syzoj.config.page.contest);
let contests = await Contest.queryPage(paginate, where, {
start_time: 'DESC'
});
await contests.forEachAsync(async x => x.subtitle = await syzoj.utils.markdown(x.subtitle));
@ -36,7 +38,7 @@ app.get('/contest/:id/edit', async (req, res) => {
if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。');
let contest_id = parseInt(req.params.id);
let contest = await Contest.fromID(contest_id);
let contest = await Contest.findById(contest_id);
if (!contest) {
contest = await Contest.create();
contest.id = 0;
@ -45,8 +47,8 @@ app.get('/contest/:id/edit', async (req, res) => {
}
let problems = [], admins = [];
if (contest.problems) problems = await contest.problems.split('|').mapAsync(async id => await Problem.fromID(id));
if (contest.admins) admins = await contest.admins.split('|').mapAsync(async id => await User.fromID(id));
if (contest.problems) problems = await contest.problems.split('|').mapAsync(async id => await Problem.findById(id));
if (contest.admins) admins = await contest.admins.split('|').mapAsync(async id => await User.findById(id));
res.render('contest_edit', {
contest: contest,
@ -66,7 +68,7 @@ app.post('/contest/:id/edit', async (req, res) => {
if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。');
let contest_id = parseInt(req.params.id);
let contest = await Contest.fromID(contest_id);
let contest = await Contest.findById(contest_id);
let ranklist = null;
if (!contest) {
contest = await Contest.create();
@ -120,7 +122,7 @@ app.get('/contest/:id', async (req, res) => {
const curUser = res.locals.user;
let contest_id = parseInt(req.params.id);
let contest = await Contest.fromID(contest_id);
let contest = await Contest.findById(contest_id);
if (!contest) throw new ErrorMessage('无此比赛。');
if (!contest.is_public && (!res.locals.user || !res.locals.user.is_admin)) throw new ErrorMessage('比赛未公开,请耐心等待 (´∀ `)');
@ -131,7 +133,7 @@ app.get('/contest/:id', async (req, res) => {
contest.information = await syzoj.utils.markdown(contest.information);
let problems_id = await contest.getProblems();
let problems = await problems_id.mapAsync(async id => await Problem.fromID(id));
let problems = await problems_id.mapAsync(async id => await Problem.findById(id));
let player = null;
@ -147,7 +149,7 @@ app.get('/contest/:id', async (req, res) => {
for (let problem of problems) {
if (contest.type === 'noi') {
if (player.score_details[problem.problem.id]) {
let judge_state = await JudgeState.fromID(player.score_details[problem.problem.id].judge_id);
let judge_state = await JudgeState.findById(player.score_details[problem.problem.id].judge_id);
problem.status = judge_state.status;
if (!contest.ended && !await problem.problem.isAllowedEditBy(res.locals.user) && !['Compile Error', 'Waiting', 'Compiling'].includes(problem.status)) {
problem.status = 'Submitted';
@ -156,7 +158,7 @@ app.get('/contest/:id', async (req, res) => {
}
} else if (contest.type === 'ioi') {
if (player.score_details[problem.problem.id]) {
let judge_state = await JudgeState.fromID(player.score_details[problem.problem.id].judge_id);
let judge_state = await JudgeState.findById(player.score_details[problem.problem.id].judge_id);
problem.status = judge_state.status;
problem.judge_id = player.score_details[problem.problem.id].judge_id;
await contest.loadRelationships();
@ -222,7 +224,7 @@ app.get('/contest/:id', async (req, res) => {
app.get('/contest/:id/ranklist', async (req, res) => {
try {
let contest_id = parseInt(req.params.id);
let contest = await Contest.fromID(contest_id);
let contest = await Contest.findById(contest_id);
const curUser = res.locals.user;
if (!contest) throw new ErrorMessage('无此比赛。');
@ -238,14 +240,14 @@ app.get('/contest/:id/ranklist', async (req, res) => {
for (let i = 1; i <= contest.ranklist.ranklist.player_num; i++) players_id.push(contest.ranklist.ranklist[i]);
let ranklist = await players_id.mapAsync(async player_id => {
let player = await ContestPlayer.fromID(player_id);
let player = await ContestPlayer.findById(player_id);
if (contest.type === 'noi' || contest.type === 'ioi') {
player.score = 0;
}
for (let i in player.score_details) {
player.score_details[i].judge_state = await JudgeState.fromID(player.score_details[i].judge_id);
player.score_details[i].judge_state = await JudgeState.findById(player.score_details[i].judge_id);
/*** XXX: Clumsy duplication, see ContestRanklist::updatePlayer() ***/
if (contest.type === 'noi' || contest.type === 'ioi') {
@ -255,7 +257,7 @@ app.get('/contest/:id/ranklist', async (req, res) => {
}
}
let user = await User.fromID(player.user_id);
let user = await User.findById(player.user_id);
return {
user: user,
@ -264,7 +266,7 @@ app.get('/contest/:id/ranklist', async (req, res) => {
});
let problems_id = await contest.getProblems();
let problems = await problems_id.mapAsync(async id => await Problem.fromID(id));
let problems = await problems_id.mapAsync(async id => await Problem.findById(id));
res.render('contest_ranklist', {
contest: contest,
@ -296,7 +298,7 @@ function getDisplayConfig(contest) {
app.get('/contest/:id/submissions', async (req, res) => {
try {
let contest_id = parseInt(req.params.id);
let contest = await Contest.fromID(contest_id);
let contest = await Contest.findById(contest_id);
if (!contest.is_public && (!res.locals.user || !res.locals.user.is_admin)) throw new ErrorMessage('比赛未公开,请耐心等待 (´∀ `)');
if (contest.isEnded()) {
@ -309,56 +311,68 @@ app.get('/contest/:id/submissions', async (req, res) => {
const curUser = res.locals.user;
let user = req.query.submitter && await User.fromName(req.query.submitter);
let where = {
submit_time: { $gte: contest.start_time, $lte: contest.end_time }
};
let query = JudgeState.createQueryBuilder();
let isFiltered = false;
if (displayConfig.showOthers) {
if (user) {
where.user_id = user.id;
query.andWhere('user_id = :user_id', { user_id: user.id });
isFiltered = true;
}
} else {
if (curUser == null || // Not logined
(user && user.id !== curUser.id)) { // Not querying himself
throw new ErrorMessage("您没有权限执行此操作");
throw new ErrorMessage("您没有权限执行此操作");
}
where.user_id = curUser.id;
query.andWhere('user_id = :user_id', { user_id: curUser.id });
isFiltered = true;
}
if (displayConfig.showScore) {
let minScore = parseInt(req.query.min_score);
let maxScore = parseInt(req.query.max_score);
if (!isNaN(minScore) || !isNaN(maxScore)) {
if (isNaN(minScore)) minScore = 0;
if (isNaN(maxScore)) maxScore = 100;
if (!(minScore === 0 && maxScore === 100)) {
where.score = {
$and: {
$gte: parseInt(minScore),
$lte: parseInt(maxScore)
}
};
}
}
let minScore = parseInt(req.body.min_score);
if (!isNaN(minScore)) query.andWhere('score >= :minScore', { minScore });
let maxScore = parseInt(req.body.max_score);
if (!isNaN(maxScore)) query.andWhere('score <= :maxScore', { maxScore });
if (!isNaN(minScore) || !isNaN(maxScore)) isFiltered = true;
}
if (req.query.language) {
if (req.query.language === 'submit-answer') where.language = '';
else where.language = req.query.language;
if (req.body.language === 'submit-answer') {
query.andWhere(new TypeORM.Brackets(qb => {
qb.orWhere('language = :language', { language: '' })
.orWhere('language IS NULL');
}));
} else if (req.body.language === 'non-submit-answer') {
query.andWhere('language != :language', { language: '' })
.andWhere('language IS NOT NULL');
} else {
query.andWhere('language = :language', { language: req.body.language })
}
isFiltered = true;
}
if (displayConfig.showResult) {
if (req.query.status) where.status = { $like: req.query.status + '%' };
if (req.query.status) {
query.andWhere('status = :status', { status: req.query.status });
isFiltered = true;
}
}
if (req.query.problem_id) where.problem_id = problems_id[parseInt(req.query.problem_id) - 1];
where.type = 1;
where.type_info = contest_id;
if (req.query.problem_id) {
problem_id = problems_id[parseInt(req.query.problem_id) - 1] || 0;
query.andWhere('problem_id = :problem_id', { problem_id })
isFiltered = true;
}
let isFiltered = !!(where.problem_id || where.user_id || where.score || where.language || where.status);
query.andWhere('type = 1')
.andWhere('type_info = :contest_id', { contest_id });
let paginate = syzoj.utils.paginate(await JudgeState.count(where), req.query.page, syzoj.config.page.judge_state);
let judge_state = await JudgeState.query(paginate, where, [['submit_time', 'desc']]);
let paginate = syzoj.utils.paginate(await JudgeState.countForPagination(query), req.query.page, syzoj.config.page.judge_state);
let judge_state = await JudgeState.queryPage(paginate, query, {
submit_time: 'DESC'
});
await judge_state.forEachAsync(async obj => {
await obj.loadRelationships();
@ -397,7 +411,7 @@ app.get('/contest/:id/submissions', async (req, res) => {
app.get('/contest/submission/:id', async (req, res) => {
try {
const id = parseInt(req.params.id);
const judge = await JudgeState.fromID(id);
const judge = await JudgeState.findById(id);
if (!judge) throw new ErrorMessage("提交记录 ID 不正确。");
const curUser = res.locals.user;
if ((!curUser) || judge.user_id !== curUser.id) throw new ErrorMessage("您没有权限执行此操作。");
@ -406,7 +420,7 @@ app.get('/contest/submission/:id', async (req, res) => {
return res.redirect(syzoj.utils.makeUrl(['submission', id]));
}
const contest = await Contest.fromID(judge.type_info);
const contest = await Contest.findById(judge.type_info);
contest.ended = contest.isEnded();
const displayConfig = getDisplayConfig(contest);
@ -418,7 +432,7 @@ app.get('/contest/submission/:id', async (req, res) => {
judge.problem.title = syzoj.utils.removeTitleTag(judge.problem.title);
if (judge.problem.type !== 'submit-answer') {
judge.codeLength = judge.code.length;
judge.codeLength = Buffer.from(judge.code).length;
judge.code = await syzoj.utils.highlight(judge.code, syzoj.languages[judge.language].highlight);
}
@ -448,7 +462,7 @@ app.get('/contest/submission/:id', async (req, res) => {
app.get('/contest/:id/problem/:pid', async (req, res) => {
try {
let contest_id = parseInt(req.params.id);
let contest = await Contest.fromID(contest_id);
let contest = await Contest.findById(contest_id);
if (!contest) throw new ErrorMessage('无此比赛。');
const curUser = res.locals.user;
@ -458,7 +472,7 @@ app.get('/contest/:id/problem/:pid', async (req, res) => {
if (!pid || pid < 1 || pid > problems_id.length) throw new ErrorMessage('无此题目。');
let problem_id = problems_id[pid - 1];
let problem = await Problem.fromID(problem_id);
let problem = await Problem.findById(problem_id);
await problem.loadRelationships();
contest.ended = contest.isEnded();
@ -497,7 +511,7 @@ app.get('/contest/:id/problem/:pid', async (req, res) => {
app.get('/contest/:id/:pid/download/additional_file', async (req, res) => {
try {
let id = parseInt(req.params.id);
let contest = await Contest.fromID(id);
let contest = await Contest.findById(id);
if (!contest) throw new ErrorMessage('无此比赛。');
let problems_id = await contest.getProblems();
@ -506,7 +520,7 @@ app.get('/contest/:id/:pid/download/additional_file', async (req, res) => {
if (!pid || pid < 1 || pid > problems_id.length) throw new ErrorMessage('无此题目。');
let problem_id = problems_id[pid - 1];
let problem = await Problem.fromID(problem_id);
let problem = await Problem.findById(problem_id);
contest.ended = contest.isEnded();
if (!(contest.isRunning() || contest.isEnded())) {

57
modules/discussion.js

@ -12,17 +12,19 @@ app.get('/discussion/:type?', async (req, res) => {
let where;
if (in_problems) {
where = { problem_id: { $not: null } };
where = { problem_id: TypeORM.Not(TypeORM.IsNull()) };
} else {
where = { problem_id: { $eq: null } };
where = { problem_id: null };
}
let paginate = syzoj.utils.paginate(await Article.count(where), req.query.page, syzoj.config.page.discussion);
let articles = await Article.query(paginate, where, [['sort_time', 'desc']]);
let paginate = syzoj.utils.paginate(await Article.countForPagination(where), req.query.page, syzoj.config.page.discussion);
let articles = await Article.queryPage(paginate, where, {
sort_time: 'DESC'
});
for (let article of articles) {
await article.loadRelationships();
if (in_problems) {
article.problem = await Problem.fromID(article.problem_id);
article.problem = await Problem.findById(article.problem_id);
}
}
@ -43,15 +45,17 @@ app.get('/discussion/:type?', async (req, res) => {
app.get('/discussion/problem/:pid', async (req, res) => {
try {
let pid = parseInt(req.params.pid);
let problem = await Problem.fromID(pid);
let problem = await Problem.findById(pid);
if (!problem) throw new ErrorMessage('无此题目。');
if (!await problem.isAllowedUseBy(res.locals.user)) {
throw new ErrorMessage('您没有权限进行此操作。');
}
let where = { problem_id: pid };
let paginate = syzoj.utils.paginate(await Article.count(where), req.query.page, syzoj.config.page.discussion);
let articles = await Article.query(paginate, where, [['sort_time', 'desc']]);
let paginate = syzoj.utils.paginate(await Article.countForPagination(where), req.query.page, syzoj.config.page.discussion);
let articles = await Article.queryPage(paginate, where, {
sort_time: 'DESC'
});
for (let article of articles) await article.loadRelationships();
@ -72,7 +76,7 @@ app.get('/discussion/problem/:pid', async (req, res) => {
app.get('/article/:id', async (req, res) => {
try {
let id = parseInt(req.params.id);
let article = await Article.fromID(id);
let article = await Article.findById(id);
if (!article) throw new ErrorMessage('无此帖子。');
await article.loadRelationships();
@ -81,10 +85,12 @@ app.get('/article/:id', async (req, res) => {
article.content = await syzoj.utils.markdown(article.content);
let where = { article_id: id };
let commentsCount = await ArticleComment.count(where);
let commentsCount = await ArticleComment.countForPagination(where);
let paginate = syzoj.utils.paginate(commentsCount, req.query.page, syzoj.config.page.article_comment);
let comments = await ArticleComment.query(paginate, where, [['public_time', 'desc']]);
let comments = await ArticleComment.queryPage(paginate, where, {
public_time: 'DESC'
});
for (let comment of comments) {
comment.content = await syzoj.utils.markdown(comment.content);
@ -94,7 +100,7 @@ app.get('/article/:id', async (req, res) => {
let problem = null;
if (article.problem_id) {
problem = await Problem.fromID(article.problem_id);
problem = await Problem.findById(article.problem_id);
if (!await problem.isAllowedUseBy(res.locals.user)) {
throw new ErrorMessage('您没有权限进行此操作。');
}
@ -120,7 +126,7 @@ app.get('/article/:id/edit', async (req, res) => {
if (!res.locals.user) throw new ErrorMessage('请登录后继续。', { '登录': syzoj.utils.makeUrl(['login'], { 'url': req.originalUrl }) });
let id = parseInt(req.params.id);
let article = await Article.fromID(id);
let article = await Article.findById(id);
if (!article) {
article = await Article.create();
@ -146,7 +152,7 @@ app.post('/article/:id/edit', async (req, res) => {
if (!res.locals.user) throw new ErrorMessage('请登录后继续。', { '登录': syzoj.utils.makeUrl(['login'], { 'url': req.originalUrl }) });
let id = parseInt(req.params.id);
let article = await Article.fromID(id);
let article = await Article.findById(id);
let time = syzoj.utils.getCurrentDate();
if (!article) {
@ -155,7 +161,7 @@ app.post('/article/:id/edit', async (req, res) => {
article.public_time = article.sort_time = time;
if (req.query.problem_id) {
let problem = await Problem.fromID(req.query.problem_id);
let problem = await Problem.findById(req.query.problem_id);
if (!problem) throw new ErrorMessage('无此题目。');
article.problem_id = problem.id;
} else {
@ -187,7 +193,7 @@ app.post('/article/:id/delete', async (req, res) => {
if (!res.locals.user) throw new ErrorMessage('请登录后继续。', { '登录': syzoj.utils.makeUrl(['login'], { 'url': req.originalUrl }) });
let id = parseInt(req.params.id);
let article = await Article.fromID(id);
let article = await Article.findById(id);
if (!article) {
throw new ErrorMessage('无此帖子。');
@ -195,6 +201,10 @@ app.post('/article/:id/delete', async (req, res) => {
if (!await article.isAllowedEditBy(res.locals.user)) throw new ErrorMessage('您没有权限进行此操作。');
}
await Promise.all((await ArticleComment.find({
article_id: article.id
})).map(comment => comment.destroy()))
await article.destroy();
res.redirect(syzoj.utils.makeUrl(['discussion', 'global']));
@ -211,7 +221,7 @@ app.post('/article/:id/comment', async (req, res) => {
if (!res.locals.user) throw new ErrorMessage('请登录后继续。', { '登录': syzoj.utils.makeUrl(['login'], { 'url': req.originalUrl }) });
let id = parseInt(req.params.id);
let article = await Article.fromID(id);
let article = await Article.findById(id);
if (!article) {
throw new ErrorMessage('无此帖子。');
@ -228,9 +238,7 @@ app.post('/article/:id/comment', async (req, res) => {
await comment.save();
article.sort_time = syzoj.utils.getCurrentDate();
article.comments_num += 1;
await article.save();
await article.resetReplyCountAndTime();
res.redirect(syzoj.utils.makeUrl(['article', article.id]));
} catch (e) {
@ -246,7 +254,7 @@ app.post('/article/:article_id/comment/:id/delete', async (req, res) => {
if (!res.locals.user) throw new ErrorMessage('请登录后继续。', { '登录': syzoj.utils.makeUrl(['login'], { 'url': req.originalUrl }) });
let id = parseInt(req.params.id);
let comment = await ArticleComment.fromID(id);
let comment = await ArticleComment.findById(id);
if (!comment) {
throw new ErrorMessage('无此评论。');
@ -254,12 +262,11 @@ app.post('/article/:article_id/comment/:id/delete', async (req, res) => {
if (!await comment.isAllowedEditBy(res.locals.user)) throw new ErrorMessage('您没有权限进行此操作。');
}
await comment.destroy();
const article = await Article.findById(comment.article_id);
let article = comment.article;
article.comments_num -= 1;
await comment.destroy();
await article.save();
await article.resetReplyCountAndTime();
res.redirect(syzoj.utils.makeUrl(['article', comment.article_id]));
} catch (e) {

17
modules/index.js

@ -10,10 +10,15 @@ const timeAgo = new TimeAgo('zh-CN');
app.get('/', async (req, res) => {
try {
let ranklist = await User.query([1, syzoj.config.page.ranklist_index], { is_show: true }, [[syzoj.config.sorting.ranklist.field, syzoj.config.sorting.ranklist.order]]);
let ranklist = await User.queryRange([1, syzoj.config.page.ranklist_index], { is_show: true }, {
[syzoj.config.sorting.ranklist.field]: syzoj.config.sorting.ranklist.order
});
await ranklist.forEachAsync(async x => x.renderInformation());
let notices = (await Article.query(null, { is_notice: true }, [['public_time', 'desc']])).map(article => ({
let notices = (await Article.find({
where: { is_notice: true },
order: { public_time: 'DESC' }
})).map(article => ({
title: article.title,
url: syzoj.utils.makeUrl(['article', article.id]),
date: syzoj.utils.formatDate(article.public_time, 'L')
@ -24,9 +29,13 @@ app.get('/', async (req, res) => {
fortune = Divine(res.locals.user.username, res.locals.user.sex);
}
let contests = await Contest.query([1, 5], { is_public: true }, [['start_time', 'desc']]);
let contests = await Contest.queryRange([1, 5], { is_public: true }, {
start_time: 'DESC'
});
let problems = (await Problem.query([1, 5], { is_public: true }, [['publicize_time', 'desc']])).map(problem => ({
let problems = (await Problem.queryRange([1, 5], { is_public: true }, {
publicize_time: 'DESC'
})).map(problem => ({
id: problem.id,
title: problem.title,
time: timeAgo.format(new Date(problem.publicize_time)),

198
modules/problem.js

@ -1,13 +1,12 @@
let Problem = syzoj.model('problem');
let JudgeState = syzoj.model('judge_state');
let FormattedCode = syzoj.model('formatted_code');
let CustomTest = syzoj.model('custom_test');
let WaitingJudge = syzoj.model('waiting_judge');
let Contest = syzoj.model('contest');
let ProblemTag = syzoj.model('problem_tag');
let ProblemTagMap = syzoj.model('problem_tag_map');
let Article = syzoj.model('article');
const Sequelize = require('sequelize');
const randomstring = require('randomstring');
const fs = require('fs-extra');
let Judger = syzoj.lib('judger');
let CodeFormatter = syzoj.lib('code_formatter');
@ -20,28 +19,24 @@ app.get('/problems', async (req, res) => {
throw new ErrorMessage('错误的排序参数。');
}
let sortVal = sort;
if (sort === 'ac_rate') {
sortVal = { raw: 'ac_num / submit_num' };
}
let where = {};
let query = Problem.createQueryBuilder();
if (!res.locals.user || !await res.locals.user.hasPrivilege('manage_problem')) {
if (res.locals.user) {
where = {
$or: {
is_public: 1,
user_id: res.locals.user.id
}
};
query.where('is_public = 1')
.orWhere('user_id = :user_id', { user_id: res.locals.user.id });
} else {
where = {
is_public: 1
};
query.where('is_public = 1');
}
}
let paginate = syzoj.utils.paginate(await Problem.count(where), req.query.page, syzoj.config.page.problem);
let problems = await Problem.query(paginate, where, [[sortVal, order]]);
if (sort === 'ac_rate') {
query.orderBy('ac_num / submit_num', order.toUpperCase());
} else {
query.orderBy(sort, order.toUpperCase());
}
let paginate = syzoj.utils.paginate(await Problem.countForPagination(query), req.query.page, syzoj.config.page.problem);
let problems = await Problem.queryPage(paginate, query);
await problems.forEachAsync(async problem => {
problem.allowedEdit = await problem.isAllowedEditBy(res.locals.user);
@ -73,45 +68,38 @@ app.get('/problems/search', async (req, res) => {
throw new ErrorMessage('错误的排序参数。');
}
let where = {
$or: {
title: { $like: `%${req.query.keyword}%` },
id: id
}
};
let query = Problem.createQueryBuilder();
if (!res.locals.user || !await res.locals.user.hasPrivilege('manage_problem')) {
if (res.locals.user) {
where = {
$and: [
where,
{
$or: {
is_public: 1,
user_id: res.locals.user.id
}
}
]
};
query.where(new TypeORM.Brackets(qb => {
qb.where('is_public = 1')
.orWhere('user_id = :user_id', { user_id: res.locals.user.id })
}))
.andWhere(new TypeORM.Brackets(qb => {
qb.where('title LIKE :title', { title: `%${req.query.keyword}%` })
.orWhere('id = :id', { id: id })
}));
} else {
where = {
$and: [
where,
{
is_public: 1
}
]
};
query.where('is_public = 1')
.andWhere(new TypeORM.Brackets(qb => {
qb.where('title LIKE :title', { title: `%${req.query.keyword}%` })
.orWhere('id = :id', { id: id })
}));
}
} else {
query.where('title LIKE :title', { title: `%${req.query.keyword}%` })
.orWhere('id = :id', { id: id })
}
let sortVal = sort;
query.orderBy('id = ' + id.toString(), 'DESC');
if (sort === 'ac_rate') {
sortVal = { raw: 'ac_num / submit_num' };
query.addOrderBy('ac_num / submit_num', order.toUpperCase());
} else {
query.addOrderBy(sort, order.toUpperCase());
}
let paginate = syzoj.utils.paginate(await Problem.count(where), req.query.page, syzoj.config.page.problem);
let problems = await Problem.query(paginate, where, [syzoj.db.literal('`id` = ' + id + ' DESC'), [sortVal, order]]);
let paginate = syzoj.utils.paginate(await Problem.countForPagination(query), req.query.page, syzoj.config.page.problem);
let problems = await Problem.queryPage(paginate, query);
await problems.forEachAsync(async problem => {
problem.allowedEdit = await problem.isAllowedEditBy(res.locals.user);
@ -137,7 +125,7 @@ app.get('/problems/search', async (req, res) => {
app.get('/problems/tag/:tagIDs', async (req, res) => {
try {
let tagIDs = Array.from(new Set(req.params.tagIDs.split(',').map(x => parseInt(x))));
let tags = await tagIDs.mapAsync(async tagID => ProblemTag.fromID(tagID));
let tags = await tagIDs.mapAsync(async tagID => ProblemTag.findById(tagID));
const sort = req.query.sort || syzoj.config.sorting.problem.field;
const order = req.query.order || syzoj.config.sorting.problem.order;
if (!['id', 'title', 'rating', 'ac_num', 'submit_num', 'ac_rate'].includes(sort) || !['asc', 'desc'].includes(order)) {
@ -157,7 +145,7 @@ app.get('/problems/tag/:tagIDs', async (req, res) => {
}
}
let sql = 'SELECT * FROM `problem` WHERE\n';
let sql = 'SELECT `id` FROM `problem` WHERE\n';
for (let tagID of tagIDs) {
if (tagID !== tagIDs[0]) {
sql += 'AND\n';
@ -174,13 +162,18 @@ app.get('/problems/tag/:tagIDs', async (req, res) => {
}
}
let paginate = syzoj.utils.paginate(await Problem.count(sql), req.query.page, syzoj.config.page.problem);
let paginate = syzoj.utils.paginate(await Problem.countQuery(sql), req.query.page, syzoj.config.page.problem);
let problems = await Problem.query(sql + ` ORDER BY ${sortVal} ${order} ` + paginate.toSQL());
await problems.forEachAsync(async problem => {
problems = await problems.mapAsync(async problem => {
// query() returns plain objects.
problem = await Problem.findById(problem.id);
problem.allowedEdit = await problem.isAllowedEditBy(res.locals.user);
problem.judge_state = await problem.getJudgeState(res.locals.user, true);
problem.tags = await problem.getTags();
return problem;
});
res.render('problems', {
@ -202,7 +195,7 @@ app.get('/problems/tag/:tagIDs', async (req, res) => {
app.get('/problem/:id', async (req, res) => {
try {
let id = parseInt(req.params.id);
let problem = await Problem.fromID(id);
let problem = await Problem.findById(id);
if (!problem) throw new ErrorMessage('无此题目。');
if (!await problem.isAllowedUseBy(res.locals.user)) {
@ -245,7 +238,7 @@ app.get('/problem/:id', async (req, res) => {
app.get('/problem/:id/export', async (req, res) => {
try {
let id = parseInt(req.params.id);
let problem = await Problem.fromID(id);
let problem = await Problem.findById(id);
if (!problem || !problem.is_public) throw new ErrorMessage('无此题目。');
let obj = {
@ -279,11 +272,15 @@ app.get('/problem/:id/export', async (req, res) => {
app.get('/problem/:id/edit', async (req, res) => {
try {
let id = parseInt(req.params.id) || 0;
let problem = await Problem.fromID(id);
let problem = await Problem.findById(id);
if (!problem) {
if (!res.locals.user) throw new ErrorMessage('请登录后继续。', { '登录': syzoj.utils.makeUrl(['login'], { 'url': req.originalUrl }) });
problem = await Problem.create();
problem = await Problem.create({
time_limit: syzoj.config.default.problem.time_limit,
memory_limit: syzoj.config.default.problem.memory_limit,
type: 'traditional'
});
problem.id = id;
problem.allowedEdit = true;
problem.tags = [];
@ -310,16 +307,20 @@ app.get('/problem/:id/edit', async (req, res) => {
app.post('/problem/:id/edit', async (req, res) => {
try {
let id = parseInt(req.params.id) || 0;
let problem = await Problem.fromID(id);
let problem = await Problem.findById(id);
if (!problem) {
if (!res.locals.user) throw new ErrorMessage('请登录后继续。', { '登录': syzoj.utils.makeUrl(['login'], { 'url': req.originalUrl }) });
problem = await Problem.create();
problem = await Problem.create({
time_limit: syzoj.config.default.problem.time_limit,
memory_limit: syzoj.config.default.problem.memory_limit,
type: 'traditional'
});
if (await res.locals.user.hasPrivilege('manage_problem')) {
let customID = parseInt(req.body.id);
if (customID) {
if (await Problem.fromID(customID)) throw new ErrorMessage('ID 已被使用。');
if (await Problem.findById(customID)) throw new ErrorMessage('ID 已被使用。');
problem.id = customID;
} else if (id) problem.id = id;
}
@ -333,7 +334,7 @@ app.post('/problem/:id/edit', async (req, res) => {
if (await res.locals.user.hasPrivilege('manage_problem')) {
let customID = parseInt(req.body.id);
if (customID && customID !== id) {
if (await Problem.fromID(customID)) throw new ErrorMessage('ID 已被使用。');
if (await Problem.findById(customID)) throw new ErrorMessage('ID 已被使用。');
await problem.changeID(customID);
}
}
@ -357,7 +358,7 @@ app.post('/problem/:id/edit', async (req, res) => {
req.body.tags = [req.body.tags];
}
let newTagIDs = await req.body.tags.map(x => parseInt(x)).filterAsync(async x => ProblemTag.fromID(x));
let newTagIDs = await req.body.tags.map(x => parseInt(x)).filterAsync(async x => ProblemTag.findById(x));
await problem.setTags(newTagIDs);
res.redirect(syzoj.utils.makeUrl(['problem', problem.id]));
@ -372,12 +373,16 @@ app.post('/problem/:id/edit', async (req, res) => {
app.get('/problem/:id/import', async (req, res) => {
try {
let id = parseInt(req.params.id) || 0;
let problem = await Problem.fromID(id);
let problem = await Problem.findById(id);
if (!problem) {
if (!res.locals.user) throw new ErrorMessage('请登录后继续。', { '登录': syzoj.utils.makeUrl(['login'], { 'url': req.originalUrl }) });
problem = await Problem.create();
problem = await Problem.create({
time_limit: syzoj.config.default.problem.time_limit,
memory_limit: syzoj.config.default.problem.memory_limit,
type: 'traditional'
});
problem.id = id;
problem.new = true;
problem.user_id = res.locals.user.id;
@ -403,16 +408,20 @@ app.get('/problem/:id/import', async (req, res) => {
app.post('/problem/:id/import', async (req, res) => {
try {
let id = parseInt(req.params.id) || 0;
let problem = await Problem.fromID(id);
let problem = await Problem.findById(id);
if (!problem) {
if (!res.locals.user) throw new ErrorMessage('请登录后继续。', { '登录': syzoj.utils.makeUrl(['login'], { 'url': req.originalUrl }) });
problem = await Problem.create();
problem = await Problem.create({
time_limit: syzoj.config.default.problem.time_limit,
memory_limit: syzoj.config.default.problem.memory_limit,
type: 'traditional'
});
if (await res.locals.user.hasPrivilege('manage_problem')) {
let customID = parseInt(req.body.id);
if (customID) {
if (await Problem.fromID(customID)) throw new ErrorMessage('ID 已被使用。');
if (await Problem.findById(customID)) throw new ErrorMessage('ID 已被使用。');
problem.id = customID;
} else if (id) problem.id = id;
}
@ -460,15 +469,14 @@ app.post('/problem/:id/import', async (req, res) => {
let download = require('download');
let tmp = require('tmp-promise');
let tmpFile = await tmp.file();
let fs = require('bluebird').promisifyAll(require('fs'));
try {
let data = await download(req.body.url + (req.body.url.endsWith('/') ? 'testdata/download' : '/testdata/download'));
await fs.writeFileAsync(tmpFile.path, data);
await fs.writeFile(tmpFile.path, data);
await problem.updateTestdata(tmpFile.path, await res.locals.user.hasPrivilege('manage_problem'));
if (json.obj.have_additional_file) {
let additional_file = await download(req.body.url + (req.body.url.endsWith('/') ? 'download/additional_file' : '/download/additional_file'));
await fs.writeFileAsync(tmpFile.path, additional_file);
await fs.writeFile(tmpFile.path, additional_file);
await problem.updateFile(tmpFile.path, 'additional_file', await res.locals.user.hasPrivilege('manage_problem'));
}
} catch (e) {
@ -488,7 +496,7 @@ app.post('/problem/:id/import', async (req, res) => {
app.get('/problem/:id/manage', async (req, res) => {
try {
let id = parseInt(req.params.id);
let problem = await Problem.fromID(id);
let problem = await Problem.findById(id);
if (!problem) throw new ErrorMessage('无此题目。');
if (!await problem.isAllowedEditBy(res.locals.user)) throw new ErrorMessage('您没有权限进行此操作。');
@ -512,7 +520,7 @@ app.get('/problem/:id/manage', async (req, res) => {
app.post('/problem/:id/manage', app.multer.fields([{ name: 'testdata', maxCount: 1 }, { name: 'additional_file', maxCount: 1 }]), async (req, res) => {
try {
let id = parseInt(req.params.id);
let problem = await Problem.fromID(id);
let problem = await Problem.findById(id);
if (!problem) throw new ErrorMessage('无此题目。');
if (!await problem.isAllowedEditBy(res.locals.user)) throw new ErrorMessage('您没有权限进行此操作。');
@ -560,7 +568,7 @@ app.post('/problem/:id/manage', app.multer.fields([{ name: 'testdata', maxCount:
async function setPublic(req, res, is_public) {
try {
let id = parseInt(req.params.id);
let problem = await Problem.fromID(id);
let problem = await Problem.findById(id);
if (!problem) throw new ErrorMessage('无此题目。');
let allowedManage = await problem.isAllowedManageBy(res.locals.user);
@ -571,10 +579,7 @@ async function setPublic(req, res, is_public) {
problem.publicize_time = new Date();
await problem.save();
JudgeState.model.update(
{ is_public: is_public },
{ where: { problem_id: id } }
);
JudgeState.query('UPDATE `judge_state` SET `is_public` = ' + is_public + ' WHERE `problem_id` = ' + id);
res.redirect(syzoj.utils.makeUrl(['problem', id]));
} catch (e) {
@ -596,7 +601,7 @@ app.post('/problem/:id/dis_public', async (req, res) => {
app.post('/problem/:id/submit', app.multer.fields([{ name: 'answer', maxCount: 1 }]), async (req, res) => {
try {
let id = parseInt(req.params.id);
let problem = await Problem.fromID(id);
let problem = await Problem.findById(id);
const curUser = res.locals.user;
if (!problem) throw new ErrorMessage('无此题目。');
@ -625,6 +630,9 @@ app.post('/problem/:id/submit', app.multer.fields([{ name: 'answer', maxCount: 1
if (!file.md5) throw new ErrorMessage('上传答案文件失败。');
judge_state = await JudgeState.create({
submit_time: parseInt((new Date()).getTime() / 1000),
status: 'Unknown',
task_id: randomstring.generate(10),
code: file.md5,
code_length: size,
language: null,
@ -636,16 +644,18 @@ app.post('/problem/:id/submit', app.multer.fields([{ name: 'answer', maxCount: 1
let code;
if (req.files['answer']) {
if (req.files['answer'][0].size > syzoj.config.limit.submit_code) throw new ErrorMessage('代码文件太大。');
let fs = Promise.promisifyAll(require('fs'));
code = (await fs.readFileAsync(req.files['answer'][0].path)).toString();
code = (await fs.readFile(req.files['answer'][0].path)).toString();
} else {
if (req.body.code.length > syzoj.config.limit.submit_code) throw new ErrorMessage('代码太长。');
if (Buffer.from(req.body.code).length > syzoj.config.limit.submit_code) throw new ErrorMessage('代码太长。');
code = req.body.code;
}
judge_state = await JudgeState.create({
submit_time: parseInt((new Date()).getTime() / 1000),
status: 'Unknown',
task_id: randomstring.generate(10),
code: code,
code_length: code.length,
code_length: Buffer.from(code).length,
language: req.body.language,
user_id: curUser.id,
problem_id: req.params.id,
@ -656,7 +666,7 @@ app.post('/problem/:id/submit', app.multer.fields([{ name: 'answer', maxCount: 1
let contest_id = parseInt(req.query.contest_id);
let contest;
if (contest_id) {
contest = await Contest.fromID(contest_id);
contest = await Contest.findById(contest_id);
if (!contest) throw new ErrorMessage('无此比赛。');
if ((!contest.isRunning()) && (!await contest.isSupervisior(curUser))) throw new ErrorMessage('比赛未开始或已结束。');
let problems_id = await contest.getProblems();
@ -721,7 +731,7 @@ app.post('/problem/:id/submit', app.multer.fields([{ name: 'answer', maxCount: 1
app.post('/problem/:id/delete', async (req, res) => {
try {
let id = parseInt(req.params.id);
let problem = await Problem.fromID(id);
let problem = await Problem.findById(id);
if (!problem) throw new ErrorMessage('无此题目。');
if (!problem.isAllowedManageBy(res.locals.user)) throw new ErrorMessage('您没有权限进行此操作。');
@ -740,7 +750,7 @@ app.post('/problem/:id/delete', async (req, res) => {
app.get('/problem/:id/testdata', async (req, res) => {
try {
let id = parseInt(req.params.id);
let problem = await Problem.fromID(id);
let problem = await Problem.findById(id);
if (!problem) throw new ErrorMessage('无此题目。');
if (!await problem.isAllowedUseBy(res.locals.user)) throw new ErrorMessage('您没有权限进行此操作。');
@ -767,7 +777,7 @@ app.get('/problem/:id/testdata', async (req, res) => {
app.post('/problem/:id/testdata/upload', app.multer.array('file'), async (req, res) => {
try {
let id = parseInt(req.params.id);
let problem = await Problem.fromID(id);
let problem = await Problem.findById(id);
if (!problem) throw new ErrorMessage('无此题目。');
if (!await problem.isAllowedEditBy(res.locals.user)) throw new ErrorMessage('您没有权限进行此操作。');
@ -790,7 +800,7 @@ app.post('/problem/:id/testdata/upload', app.multer.array('file'), async (req, r
app.post('/problem/:id/testdata/delete/:filename', async (req, res) => {
try {
let id = parseInt(req.params.id);
let problem = await Problem.fromID(id);
let problem = await Problem.findById(id);
if (!problem) throw new ErrorMessage('无此题目。');
if (!await problem.isAllowedEditBy(res.locals.user)) throw new ErrorMessage('您没有权限进行此操作。');
@ -809,7 +819,7 @@ app.post('/problem/:id/testdata/delete/:filename', async (req, res) => {
app.get('/problem/:id/testdata/download/:filename?', async (req, res) => {
try {
let id = parseInt(req.params.id);
let problem = await Problem.fromID(id);
let problem = await Problem.findById(id);
if (!problem) throw new ErrorMessage('无此题目。');
if (!await problem.isAllowedUseBy(res.locals.user)) throw new ErrorMessage('您没有权限进行此操作。');
@ -836,14 +846,14 @@ app.get('/problem/:id/testdata/download/:filename?', async (req, res) => {
app.get('/problem/:id/download/additional_file', async (req, res) => {
try {
let id = parseInt(req.params.id);
let problem = await Problem.fromID(id);
let problem = await Problem.findById(id);
if (!problem) throw new ErrorMessage('无此题目。');
// XXX: Reduce duplication (see the '/problem/:id/submit' handler)
let contest_id = parseInt(req.query.contest_id);
if (contest_id) {
let contest = await Contest.fromID(contest_id);
let contest = await Contest.findById(contest_id);
if (!contest) throw new ErrorMessage('无此比赛。');
if (!contest.isRunning()) throw new ErrorMessage('比赛未开始或已结束。');
let problems_id = await contest.getProblems();
@ -869,7 +879,7 @@ app.get('/problem/:id/download/additional_file', async (req, res) => {
app.get('/problem/:id/statistics/:type', async (req, res) => {
try {
let id = parseInt(req.params.id);
let problem = await Problem.fromID(id);
let problem = await Problem.findById(id);
if (!problem) throw new ErrorMessage('无此题目。');
if (!await problem.isAllowedUseBy(res.locals.user)) throw new ErrorMessage('您没有权限进行此操作。');
@ -899,7 +909,7 @@ app.get('/problem/:id/statistics/:type', async (req, res) => {
app.post('/problem/:id/custom-test', app.multer.fields([{ name: 'code_upload', maxCount: 1 }, { name: 'input_file', maxCount: 1 }]), async (req, res) => {
try {
let id = parseInt(req.params.id);
let problem = await Problem.fromID(id);
let problem = await Problem.findById(id);
if (!problem) throw new ErrorMessage('无此题目。');
if (!res.locals.user) throw new ErrorMessage('请登录后继续。', { '登录': syzoj.utils.makeUrl(['login'], { 'url': syzoj.utils.makeUrl(['problem', id]) }) });
@ -920,7 +930,7 @@ app.post('/problem/:id/custom-test', app.multer.fields([{ name: 'code_upload', m
if (req.files['code_upload'][0].size > syzoj.config.limit.submit_code) throw new ErrorMessage('代码过长。');
code = (await require('fs-extra').readFileAsync(req.files['code_upload'][0].path)).toString();
} else {
if (req.body.code.length > syzoj.config.limit.submit_code) throw new ErrorMessage('代码过长。');
if (Buffer.from(req.body.code).length > syzoj.config.limit.submit_code) throw new ErrorMessage('代码过长。');
code = req.body.code;
}

4
modules/problem_tag.js

@ -5,7 +5,7 @@ app.get('/problems/tag/:id/edit', async (req, res) => {
if (!res.locals.user || !await res.locals.user.hasPrivilege('manage_problem_tag')) throw new ErrorMessage('您没有权限进行此操作。');
let id = parseInt(req.params.id) || 0;
let tag = await ProblemTag.fromID(id);
let tag = await ProblemTag.findById(id);
if (!tag) {
tag = await ProblemTag.create();
@ -28,7 +28,7 @@ app.post('/problems/tag/:id/edit', async (req, res) => {
if (!res.locals.user || !await res.locals.user.hasPrivilege('manage_problem_tag')) throw new ErrorMessage('您没有权限进行此操作。');
let id = parseInt(req.params.id) || 0;
let tag = await ProblemTag.fromID(id);
let tag = await ProblemTag.findById(id);
if (!tag) {
tag = await ProblemTag.create();

113
modules/submission.js

@ -23,23 +23,32 @@ const displayConfig = {
app.get('/submissions', async (req, res) => {
try {
const curUser = res.locals.user;
let user = await User.fromName(req.query.submitter || '');
let where = {};
let query = JudgeState.createQueryBuilder();
let isFiltered = false;
let inContest = false;
if (user) where.user_id = user.id;
else if (req.query.submitter) where.user_id = -1;
let user = await User.fromName(req.query.submitter || '');
if (user) {
query.andWhere('user_id = :user_id', { user_id: user.id });
isFiltered = true;
} else if (req.query.submitter) {
query.andWhere('user_id = :user_id', { user_id: 0 });
isFiltered = true;
}
if (!req.query.contest) {
where.type = { $eq: 0 };
query.andWhere('type = 0');
} else {
const contestId = Number(req.query.contest);
const contest = await Contest.fromID(contestId);
const contest = await Contest.findById(contestId);
contest.ended = contest.isEnded();
if ((contest.ended && contest.is_public) || // If the contest is ended and is not hidden
(curUser && await contest.isSupervisior(curUser)) // Or if the user have the permission to check
) {
where.type = { $eq: 1 };
where.type_info = { $eq: contestId };
query.andWhere('type = 1');
query.andWhere('type_info = :type_info', { type_info: contestId });
inContest = true;
} else {
throw new Error("您暂时无权查看此比赛的详细评测信息。");
@ -47,64 +56,77 @@ app.get('/submissions', async (req, res) => {
}
let minScore = parseInt(req.query.min_score);
if (!isNaN(minScore)) query.andWhere('score >= :minScore', { minScore });
let maxScore = parseInt(req.query.max_score);
if (!isNaN(maxScore)) query.andWhere('score <= :maxScore', { maxScore });
if (!isNaN(minScore) || !isNaN(maxScore)) {
if (isNaN(minScore)) minScore = 0;
if (isNaN(maxScore)) maxScore = 100;
if (!(minScore === 0 && maxScore === 100)) {
where.score = {
$and: {
$gte: parseInt(minScore),
$lte: parseInt(maxScore)
}
};
if (!isNaN(minScore) || !isNaN(maxScore)) isFiltered = true;
if (req.query.language) {
if (req.query.language === 'submit-answer') {
query.andWhere(new TypeORM.Brackets(qb => {
qb.orWhere('language = :language', { language: '' })
.orWhere('language IS NULL');
}));
isFiltered = true;
} else if (req.query.language === 'non-submit-answer') {
query.andWhere('language != :language', { language: '' })
.andWhere('language IS NOT NULL');
isFiltered = true;
} else {
query.andWhere('language = :language', { language: req.query.language });
}
}
if (req.query.language) {
if (req.query.language === 'submit-answer') where.language = { $or: [{ $eq: '', }, { $eq: null }] };
else if (req.query.language === 'non-submit-answer') where.language = { $not: '' };
else where.language = req.query.language;
if (req.query.status) {
query.andWhere('status = :status', { status: req.query.status });
isFiltered = true;
}
if (req.query.status) where.status = { $like: req.query.status + '%' };
if (!inContest && (!curUser || !await curUser.hasPrivilege('manage_problem'))) {
if (req.query.problem_id) {
let problem_id = parseInt(req.query.problem_id);
let problem = await Problem.fromID(problem_id);
let problem = await Problem.findById(problem_id);
if (!problem)
throw new ErrorMessage("无此题目。");
if (await problem.isAllowedUseBy(res.locals.user)) {
where.problem_id = {
$and: [
{ $eq: where.problem_id = problem_id }
]
};
query.andWhere('problem_id = :problem_id', { problem_id: parseInt(req.query.problem_id) || 0 });
isFiltered = true;
} else {
throw new ErrorMessage("您没有权限进行此操作。");
}
} else {
where.is_public = {
$eq: true,
};
query.andWhere('is_public = true');
}
} else {
if (req.query.problem_id) where.problem_id = parseInt(req.query.problem_id) || -1;
} else if (req.query.problem_id) {
query.andWhere('problem_id = :problem_id', { problem_id: parseInt(req.query.problem_id) || 0 });
isFiltered = true;
}
let isFiltered = !!(where.problem_id || where.user_id || where.score || where.language || where.status);
let judge_state, paginate;
let paginate = syzoj.utils.paginate(await JudgeState.count(where), req.query.page, syzoj.config.page.judge_state);
let judge_state = await JudgeState.query(paginate, where, [['id', 'desc']], true);
if (syzoj.config.submissions_page_fast_pagination) {
const queryResult = await JudgeState.queryPageFast(query, syzoj.utils.paginateFast(
req.query.currPageTop, req.query.currPageBottom, syzoj.config.page.judge_state
), -1, parseInt(req.query.page));
judge_state = queryResult.data;
paginate = queryResult.meta;
} else {
paginate = syzoj.utils.paginate(
await JudgeState.countQuery(query),
req.query.page,
syzoj.config.page.judge_state
);
judge_state = await JudgeState.queryPage(paginate, query, { id: "DESC" }, true);
}
await judge_state.forEachAsync(async obj => {
await obj.loadRelationships();
if (obj.problem.type !== 'submit-answer') obj.code_length = obj.code.length;
})
if (obj.problem.type !== 'submit-answer') obj.code_length = Buffer.from(obj.code).length;
});
res.render('submissions', {
// judge_state: judge_state,
items: judge_state.map(x => ({
info: getSubmissionInfo(x, displayConfig),
token: (x.pending && x.task_id != null) ? jwt.sign({
@ -119,7 +141,8 @@ app.get('/submissions', async (req, res) => {
pushType: 'rough',
form: req.query,
displayConfig: displayConfig,
isFiltered: isFiltered
isFiltered: isFiltered,
fast_pagination: syzoj.config.submissions_page_fast_pagination
});
} catch (e) {
syzoj.log(e);
@ -132,14 +155,14 @@ app.get('/submissions', async (req, res) => {
app.get('/submission/:id', async (req, res) => {
try {
const id = parseInt(req.params.id);
const judge = await JudgeState.fromID(id);
const judge = await JudgeState.findById(id);
if (!judge) throw new ErrorMessage("提交记录 ID 不正确。");
const curUser = res.locals.user;
if (!await judge.isAllowedVisitBy(curUser)) throw new ErrorMessage('您没有权限进行此操作。');
let contest;
if (judge.type === 1) {
contest = await Contest.fromID(judge.type_info);
contest = await Contest.findById(judge.type_info);
contest.ended = contest.isEnded();
if ((!contest.ended || !contest.is_public) &&
@ -151,7 +174,7 @@ app.get('/submission/:id', async (req, res) => {
await judge.loadRelationships();
if (judge.problem.type !== 'submit-answer') {
judge.code_length = judge.code.length;
judge.code_length = Buffer.from(judge.code).length;
let key = syzoj.utils.getFormattedCodeKey(judge.code, judge.language);
if (key) {
@ -194,7 +217,7 @@ app.get('/submission/:id', async (req, res) => {
app.post('/submission/:id/rejudge', async (req, res) => {
try {
let id = parseInt(req.params.id);
let judge = await JudgeState.fromID(id);
let judge = await JudgeState.findById(id);
if (judge.pending && !(res.locals.user && await res.locals.user.hasPrivilege('manage_problem'))) throw new ErrorMessage('无法重新评测一个评测中的提交。');

17
modules/user.js

@ -12,8 +12,8 @@ app.get('/ranklist', async (req, res) => {
if (!['ac_num', 'rating', 'id', 'username'].includes(sort) || !['asc', 'desc'].includes(order)) {
throw new ErrorMessage('错误的排序参数。');
}
let paginate = syzoj.utils.paginate(await User.count({ is_show: true }), req.query.page, syzoj.config.page.ranklist);
let ranklist = await User.query(paginate, { is_show: true }, [[sort, order]]);
let paginate = syzoj.utils.paginate(await User.countForPagination({ is_show: true }), req.query.page, syzoj.config.page.ranklist);
let ranklist = await User.queryPage(paginate, { is_show: true }, { [sort]: order.toUpperCase() });
await ranklist.forEachAsync(async x => x.renderInformation());
res.render('ranklist', {
@ -76,7 +76,7 @@ app.post('/logout', async (req, res) => {
app.get('/user/:id', async (req, res) => {
try {
let id = parseInt(req.params.id);
let user = await User.fromID(id);
let user = await User.findById(id);
if (!user) throw new ErrorMessage('无此用户。');
user.ac_problems = await user.getACProblems();
user.articles = await user.getArticles();
@ -86,7 +86,10 @@ app.get('/user/:id', async (req, res) => {
await user.renderInformation();
user.emailVisible = user.public_email || user.allowedEdit;
const ratingHistoryValues = await RatingHistory.query(null, { user_id: user.id }, [['rating_calculation_id', 'asc']]);
const ratingHistoryValues = await RatingHistory.find({
where: { user_id: user.id },
order: { rating_calculation_id: 'ASC' }
});
const ratingHistories = [{
contestName: "初始积分",
value: syzoj.config.default.user.rating,
@ -95,7 +98,7 @@ app.get('/user/:id', async (req, res) => {
}];
for (const history of ratingHistoryValues) {
const contest = await Contest.fromID((await RatingCalculation.fromID(history.rating_calculation_id)).contest_id);
const contest = await Contest.findById((await RatingCalculation.findById(history.rating_calculation_id)).contest_id);
ratingHistories.push({
contestName: contest.title,
value: history.rating_after,
@ -122,7 +125,7 @@ app.get('/user/:id', async (req, res) => {
app.get('/user/:id/edit', async (req, res) => {
try {
let id = parseInt(req.params.id);
let user = await User.fromID(id);
let user = await User.findById(id);
if (!user) throw new ErrorMessage('无此用户。');
let allowedEdit = await user.isAllowedEditBy(res.locals.user);
@ -156,7 +159,7 @@ app.post('/user/:id/edit', async (req, res) => {
let user;
try {
let id = parseInt(req.params.id);
user = await User.fromID(id);
user = await User.findById(id);
if (!user) throw new ErrorMessage('无此用户。');
let allowedEdit = await user.isAllowedEditBy(res.locals.user);

11
package.json

@ -4,6 +4,7 @@
"description": "An OnlineJudge System for OI",
"main": "app.js",
"scripts": {
"prepublish": "tsc -p .",
"start": "node app.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
@ -23,13 +24,17 @@
},
"homepage": "https://github.com/syzoj/syzoj#readme",
"dependencies": {
"@types/fs-extra": "^5.0.5",
"@types/lru-cache": "^5.1.0",
"ansi-to-html": "^0.6.10",
"async-lock": "^1.2.0",
"bluebird": "^3.5.4",
"body-parser": "^1.15.2",
"cheerio": "^1.0.0-rc.1",
"command-line-args": "^5.1.0",
"cookie-parser": "^1.4.4",
"cssfilter": "0.0.10",
"deepcopy": "^2.0.0",
"download": "^7.1.0",
"ejs": "^2.5.2",
"express": "^4.14.0",
@ -42,20 +47,22 @@
"jsdom": "^14.0.0",
"jsondiffpatch": "0.2.5",
"jsonwebtoken": "^8.5.1",
"lru-cache": "^5.1.1",
"mariadb": "^2.0.3",
"moment": "^2.24.0",
"msgpack-lite": "^0.1.26",
"multer": "^1.2.0",
"mysql2": "^1.6.5",
"node-7z": "^0.4.0",
"nodemailer": "^4.7.0",
"object-assign-deep": "^0.4.0",
"object-hash": "^1.3.1",
"randomstring": "^1.1.5",
"redis": "^2.8.0",
"reflect-metadata": "^0.1.13",
"request": "^2.74.0",
"request-promise": "^4.2.4",
"sendmail": "^1.1.1",
"sequelize": "^5.1.1",
"serialize-javascript": "^1.6.1",
"session-file-store": "^1.0.0",
"socket.io": "^2.2.0",
@ -64,6 +71,8 @@
"syzoj-renderer": "^1.0.5",
"tempfile": "^2.0.0",
"tmp-promise": "^1.0.3",
"typeorm": "^0.2.16",
"typescript": "^3.4.3",
"uuid": "^3.3.2",
"waliyun": "^3.1.1",
"winston": "^3.2.1",

11
tsconfig.json

@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "commonjs",
"preserveConstEnums": true,
"sourceMap": true,
"outDir": "./models-built",
"rootDir": "./models",
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
}

21
utility.js

@ -19,7 +19,6 @@ let Promise = require('bluebird');
let path = require('path');
let fs = Promise.promisifyAll(require('fs-extra'));
let util = require('util');
let markdownRenderer = require('./libs/markdown');
let moment = require('moment');
let url = require('url');
let querystring = require('querystring');
@ -27,6 +26,7 @@ let gravatar = require('gravatar');
let filesize = require('file-size');
let AsyncLock = require('async-lock');
let JSDOM = require('jsdom').JSDOM;
let renderer = require('./libs/renderer');
module.exports = {
resolvePath(s) {
@ -56,13 +56,13 @@ module.exports = {
return new Promise((resolve, reject) => {
if (!keys) {
if (!obj || !obj.trim()) resolve("");
else markdownRenderer(obj, s => {
else renderer.markdown(obj, s => {
resolve(replaceUI(s));
});
} else {
let res = obj, cnt = keys.length;
for (let key of keys) {
markdownRenderer(res[key], (s) => {
renderer.markdown(res[key], (s) => {
res[key] = replaceUI(s);
if (!--cnt) resolve(res);
});
@ -133,7 +133,7 @@ module.exports = {
},
highlight(code, lang) {
return new Promise((resolve, reject) => {
require('./libs/highlight')(code, lang, res => {
renderer.highlight(code, lang, res => {
resolve(res);
});
});
@ -245,6 +245,19 @@ module.exports = {
}
};
},
paginateFast(currPageTop, currPageBottom, perPage) {
function parseIntOrNull(x) {
if (typeof x === 'string') x = parseInt(x);
if (typeof x !== 'number' || isNaN(x)) return null;
return x;
}
return {
currPageTop: parseIntOrNull(currPageTop),
currPageBottom: parseIntOrNull(currPageBottom),
perPage
};
},
removeTitleTag(s) {
return s.replace(/「[\S\s]+?」/, '');
},

23
views/page_fast.ejs

@ -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>
<% } %>

6
views/submissions.ejs

@ -116,7 +116,11 @@
</div>
<% } else { %>
<br>
<% include page %>
<% if (fast_pagination) { %>
<% include page_fast %>
<% } else { %>
<% include page %>
<% } %>
<% } %>
</div>

638
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save