Browse Source

Merge pull request #6 from syzoj/submit-answer

Submit answer (and very much more)
pull/6/head
Menci 7 years ago committed by GitHub
parent
commit
840d371023
  1. 24
      config-example.json
  2. 6
      models/article.js
  3. 7
      models/contest.js
  4. 115
      models/file.js
  5. 21
      models/judge_state.js
  6. 213
      models/problem.js
  7. 64
      models/testdata.js
  8. 2
      models/user.js
  9. 124
      modules/api.js
  10. 15
      modules/contest.js
  11. 3
      modules/discussion.js
  12. 22
      modules/index.js
  13. 185
      modules/problem.js
  14. 17
      modules/submission.js
  15. 1
      modules/user.js
  16. 3
      package.json
  17. 4
      static/style.css
  18. 0
      uploads/additional_file/.gitkeep
  19. 0
      uploads/answer/.gitkeep
  20. 0
      uploads/testdata/.gitkeep
  21. 166
      utility.js
  22. 48
      views/article.ejs
  23. 7
      views/article_edit.ejs
  24. 13
      views/contest_edit.ejs
  25. 10
      views/index.ejs
  26. 140
      views/problem.ejs
  27. 289
      views/problem_data.ejs
  28. 144
      views/problem_manage.ejs
  29. 42
      views/problem_testcases.ejs
  30. 27
      views/sign_up.ejs
  31. 34
      views/statistics.ejs
  32. 29
      views/submission_content.ejs
  33. 3
      views/submissions.ejs
  34. 5
      views/submissions_item.ejs

24
config-example.json

@ -10,6 +10,11 @@
"dialect": "sqlite",
"storage": "syzoj.db"
},
"register_mail": {
"enabled": true,
"address": "test@test.domain",
"key": "test"
},
"upload_dir": "uploads",
"default": {
"problem": {
@ -23,7 +28,10 @@
"limit": {
"time_limit": 10000,
"memory_limit": 1024,
"data_size": 209715200
"data_size": 209715200,
"testdata": 209715200,
"submit_code": 102400,
"submit_answer": 10485760
},
"page": {
"problem": 50,
@ -134,26 +142,12 @@
"editor": "vbscript"
}
},
"notices": [
{
"type": "// article",
"id": 1,
"comment": "Specify the id if you want to show a article"
},
{
"type": "// link",
"url": "",
"date": "",
"comment": "Specify the url and date if you want to show any link"
}
],
"links": [
{
"title": "LibreOJ",
"url": "https://loj.ac/"
}
],
"announcement": "Here is the announcement",
"session_secret": "233",
"judge_token": "233"
}

6
models/article.js

@ -38,6 +38,8 @@ let model = db.define('article', {
comments_num: { type: Sequelize.INTEGER },
allow_comment: { type: Sequelize.BOOLEAN },
is_notice: { type: Sequelize.BOOLEAN }
}, {
timestamps: false,
tableName: 'article',
@ -65,7 +67,9 @@ class Article extends Model {
sort_time: 0,
comments_num: 0,
allow_comment: true
allow_comment: true,
is_notice: false
}, val)));
}

7
models/contest.js

@ -53,7 +53,9 @@ let model = db.define('contest', {
model: 'contest_ranklist',
key: 'id'
}
}
},
is_public: { type: Sequelize.BOOLEAN }
}, {
timestamps: false,
tableName: 'contest',
@ -79,7 +81,8 @@ class Contest extends Model {
start_time: 0,
end_time: 0,
holder: 0,
ranklist_id: 0
ranklist_id: 0,
is_public: false
}, val)));
}

115
models/file.js

@ -0,0 +1,115 @@
/*
* This file is part of SYZOJ.
*
* Copyright (c) 2016 Menci <huanghaorui301@gmail.com>
*
* SYZOJ is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* SYZOJ is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with SYZOJ. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';
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)));
}
getPath() {
return File.resolvePath(this.type, this.md5);
}
static resolvePath(type, md5) {
return syzoj.utils.resolvePath(syzoj.config.upload_dir, type, md5);
}
static async upload(path, type) {
let fs = Promise.promisifyAll(require('fs-extra'));
let buf = await fs.readFileAsync(path);
if (buf.length > syzoj.config.limit.data_size) throw new ErrorMessage('数据包太大。');
try {
let AdmZip = require('adm-zip');
let zip = new AdmZip(buf);
this.unzipSize = 0;
for (let x of zip.getEntries()) this.unzipSize += x.header.size;
} catch (e) {
this.unzipSize = null;
}
let key = syzoj.utils.md5(buf);
await fs.moveAsync(path, File.resolvePath(type, key), { overwrite: true });
let file = await File.findOne({ where: { md5: key } });
if (!file) {
file = await File.create({
type: type,
md5: key
});
await file.save();
}
return file;
}
async getUnzipSize() {
if (this.unzipSize === undefined) {
try {
let fs = Promise.promisifyAll(require('fs-extra'));
let buf = await fs.readFileAsync(this.getPath());
let AdmZip = require('adm-zip');
let zip = new AdmZip(buf);
this.unzipSize = 0;
for (let x of zip.getEntries()) this.unzipSize += x.header.size;
} catch (e) {
this.unzipSize = null;
}
}
if (this.unzipSize === null) throw new ErrorMessage('无效的 ZIP 文件。');
else return this.unzipSize;
}
getModel() { return model; }
}
File.model = model;
module.exports = File;

21
models/judge_state.js

@ -28,6 +28,8 @@ let Contest = syzoj.model('contest');
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) },
@ -163,8 +165,11 @@ class JudgeState extends Model {
this.score = result.score;
this.pending = result.pending;
this.status = result.status;
this.total_time = result.total_time;
this.max_memory = result.max_memory;
if (this.language) {
// language is empty if it's a submit-answer problem
this.total_time = result.total_time;
this.max_memory = result.max_memory;
}
this.result = result;
}
@ -203,8 +208,11 @@ class JudgeState extends Model {
this.status = 'Waiting';
this.score = 0;
this.total_time = 0;
this.max_memory = 0;
if (this.language) {
// language is empty if it's a submit-answer problem
this.total_time = 0;
this.max_memory = 0;
}
this.pending = true;
this.result = { status: "Waiting", total_time: 0, max_memory: 0, score: 0, case_num: 0, compiler_output: "", pending: true };
await this.save();
@ -233,6 +241,11 @@ class JudgeState extends Model {
});
}
async getProblemType() {
await this.loadRelationships();
return this.problem.type;
}
getModel() { return model; }
}

213
models/problem.js

@ -144,6 +144,56 @@ 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 \
'
};
@ -151,7 +201,7 @@ let Sequelize = require('sequelize');
let db = syzoj.db;
let User = syzoj.model('user');
let TestData = syzoj.model('testdata');
let File = syzoj.model('file');
let model = db.define('problem', {
id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true },
@ -182,13 +232,7 @@ let model = db.define('problem', {
time_limit: { type: Sequelize.INTEGER },
memory_limit: { type: Sequelize.INTEGER },
testdata_id: {
type: Sequelize.INTEGER,
references: {
model: 'file',
key: 'id'
}
},
additional_file_id: { type: Sequelize.INTEGER },
ac_num: { type: Sequelize.INTEGER },
submit_num: { type: Sequelize.INTEGER },
@ -196,7 +240,12 @@ let model = db.define('problem', {
file_io: { type: Sequelize.BOOLEAN },
file_io_input_name: { type: Sequelize.TEXT },
file_io_output_name: { type: Sequelize.TEXT }
file_io_output_name: { type: Sequelize.TEXT },
type: {
type: Sequelize.ENUM,
values: ['traditional', 'submit-answer', 'interaction']
}
}, {
timestamps: false,
tableName: 'problem',
@ -234,14 +283,16 @@ class Problem extends Model {
file_io: false,
file_io_input_name: '',
file_io_output_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.testdata = await TestData.fromID(this.testdata_id);
this.additional_file = await File.fromID(this.additional_file_id);
}
async isAllowedEditBy(user) {
@ -263,35 +314,122 @@ class Problem extends Model {
return user.is_admin;
}
getTestdataPath() {
return syzoj.utils.resolvePath(syzoj.config.upload_dir, 'testdata', this.id.toString());
}
async updateTestdata(path) {
let AdmZip = require('adm-zip');
let zip = new AdmZip(path);
let unzipSize = 0;
for (let x of zip.getEntries()) unzipSize += x.header.size;
if (unzipSize > syzoj.config.limit.testdata) throw new ErrorMessage('数据包太大。');
let dir = this.getTestdataPath();
let fs = Promise.promisifyAll(require('fs-extra'));
await fs.removeAsync(dir);
await fs.ensureDirAsync(dir);
zip.extractAllTo(dir);
await fs.moveAsync(path, dir + '.zip', { overwrite: true });
}
async uploadTestdataSingleFile(filename, filepath, size) {
let dir = this.getTestdataPath();
let fs = Promise.promisifyAll(require('fs-extra')), path = require('path');
await fs.ensureDirAsync(dir);
let oldSize = 0;
let list = await this.listTestdata();
if (list) {
for (let file of list.files) if (file.filename !== filename) oldSize += file.size;
}
if (oldSize + size > syzoj.config.limit.testdata) throw new ErrorMessage('数据包太大。');
await fs.moveAsync(filepath, path.join(dir, filename), { overwrite: true });
await fs.removeAsync(dir + '.zip');
}
async deleteTestdataSingleFile(filename) {
let dir = this.getTestdataPath();
let fs = Promise.promisifyAll(require('fs-extra')), path = require('path');
await fs.removeAsync(path.join(dir, filename));
await fs.removeAsync(dir + '.zip');
}
async makeTestdataZip() {
let dir = this.getTestdataPath();
if (!await syzoj.utils.isDir(dir)) throw new ErrorMessage('无测试数据。');
let buf = await fs.readFileAsync(path);
let AdmZip = require('adm-zip');
let zip = new AdmZip();
if (buf.length > syzoj.config.limit.data_size) throw new ErrorMessage('测试数据太大。');
let list = await this.listTestdata();
for (let file of list.files) zip.addLocalFile(require('path').join(dir, file.filename), '', file.filename);
zip.writeZip(dir + '.zip');
}
async hasSpecialJudge() {
try {
let fs = Promise.promisifyAll(require('fs-extra'));
let dir = this.getTestdataPath();
let list = await fs.readdirAsync(dir);
return list.includes('spj.js') || list.find(x => x.startsWith('spj_')) !== undefined;
} catch (e) {
return false;
}
}
async listTestdata() {
try {
let fs = Promise.promisifyAll(require('fs-extra')), path = require('path');
let dir = this.getTestdataPath();
let list = await fs.readdirAsync(dir);
list = await list.mapAsync(async x => {
let stat = await fs.statAsync(path.join(dir, x));
if (!stat.isFile()) return undefined;
return {
filename: x,
size: stat.size
};
});
list = list.filter(x => x);
let key = syzoj.utils.md5(buf);
await fs.moveAsync(path, TestData.resolvePath(key), { overwrite: true });
let res = {
files: list,
zip: null
};
if (this.testdata_id) {
let tmp = this.testdata_id;
this.testdata_id = null;
await this.save();
try {
let stat = await fs.statAsync(this.getTestdataPath() + '.zip');
if (stat.isFile()) {
res.zip = {
size: stat.size
};
}
} catch (e) {
if (list) {
res.zip = {
size: null
};
}
}
let file = await TestData.fromID(tmp);
if (file) await file.destroy();
return res;
} catch (e) {
return null;
}
}
let filename = `test_data_${this.id}.zip`;
let file = await TestData.findOne({ where: { filename: filename } });
if (file) await file.destroy();
async updateFile(path, type) {
let file = await File.upload(path, type);
file = await TestData.create({
filename: filename,
md5: key
});
await file.save();
this.testdata_id = file.id;
if (type === 'additional_file') {
this.additional_file_id = file.id;
}
await this.save();
}
@ -453,7 +591,6 @@ class Problem extends Model {
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 `file` SET `filename` = ' + `"test_data_${id}.zip"` + ' WHERE `filename` = ' + `"test_data_${this.id}.zip"`);
let Contest = syzoj.model('contest');
let contests = await Contest.all();
@ -474,7 +611,21 @@ class Problem extends Model {
}
}
let oldTestdataDir = this.getTestdataPath(), oldTestdataZip = oldTestdataDir + '.zip';
this.id = id;
// Move testdata
let newTestdataDir = this.getTestdataPath(), newTestdataZip = newTestdataDir + '.zip';
let fs = Promise.promisifyAll(require('fs-extra'));
if (await syzoj.utils.isDir(oldTestdataDir)) {
await fs.moveAsync(oldTestdataDir, newTestdataDir);
}
if (await syzoj.utils.isFile(oldTestdataZip)) {
await fs.moveAsync(oldTestdataZip, newTestdataZip);
}
await this.save();
}

64
models/testdata.js

@ -1,64 +0,0 @@
/*
* This file is part of SYZOJ.
*
* Copyright (c) 2016 Menci <huanghaorui301@gmail.com>
*
* SYZOJ is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* SYZOJ is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with SYZOJ. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';
let Sequelize = require('sequelize');
let db = syzoj.db;
let model = db.define('file', {
id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true },
filename: { type: Sequelize.STRING(80), unique: true },
md5: { type: Sequelize.STRING(80), unique: true }
}, {
timestamps: false,
tableName: 'file',
indexes: [
{
fields: ['filename'],
},
{
fields: ['md5'],
}
]
});
let Model = require('./common');
class TestData extends Model {
static create(val) {
return TestData.fromRecord(TestData.model.build(Object.assign({
filename: '',
md5: ''
}, val)));
}
getPath() {
return TestData.resolvePath(this.md5);
}
static resolvePath(md5) {
return syzoj.utils.resolvePath(syzoj.config.upload_dir, 'testdata', md5);
}
getModel() { return model; }
}
TestData.model = model;
module.exports = TestData;

2
models/user.js

@ -197,8 +197,6 @@ class User extends Model {
let oldPrivileges = await this.getPrivileges();
console.log(newPrivileges);
let delPrivileges = oldPrivileges.filter(x => !newPrivileges.includes(x));
let addPrivileges = newPrivileges.filter(x => !oldPrivileges.includes(x));

124
modules/api.js

@ -23,7 +23,7 @@ let User = syzoj.model('user');
let Problem = syzoj.model('problem');
let WaitingJudge = syzoj.model('waiting_judge');
let JudgeState = syzoj.model('judge_state');
let TestData = syzoj.model('testdata');
let File = syzoj.model('file');
function setLoginCookie(username, password, res) {
res.cookie('login', JSON.stringify([username, password]));
@ -54,6 +54,9 @@ app.post('/api/sign_up', async (req, res) => {
res.setHeader('Content-Type', 'application/json');
let user = await User.fromName(req.body.username);
if (user) throw 2008;
user = await User.findOne({ where: { email: req.body.email } });
if (user) throw 2009;
// Because the salt is "syzoj2_xxx" and the "syzoj2_xxx" 's md5 is"59cb..."
// the empty password 's md5 will equal "59cb.."
@ -62,20 +65,87 @@ app.post('/api/sign_up', async (req, res) => {
if (!(req.body.email = req.body.email.trim())) throw 2006;
if (!syzoj.utils.isValidUsername(req.body.username)) throw 2002;
if (syzoj.config.register_mail.enabled) {
let sendmail = Promise.promisify(require('sendmail')());
let sendObj = {
username: req.body.username,
password: req.body.password,
email: req.body.email,
prevUrl: req.body.prevUrl,
r: Math.random()
};
let encrypted = encodeURIComponent(syzoj.utils.encrypt(JSON.stringify(sendObj), syzoj.config.register_mail.key).toString('base64'));
let url = req.protocol + '://' + req.get('host') + syzoj.utils.makeUrl(['api', 'sign_up', encrypted]);
try {
await sendmail({
from: syzoj.config.register_mail.address,
to: req.body.email,
type: 'text/html',
subject: `${req.body.username}${syzoj.config.title} 注册验证邮件`,
html: `<p>请点击该链接完成您在 ${syzoj.config.title} 的注册:<a href="${url}">${url}</a>。</p><p>如果您不是 ${req.body.username},请忽略此邮件。</p>`
});
} catch (e) {
throw 2010
}
res.send(JSON.stringify({ error_code: 2 }));
} else {
user = await User.create({
username: req.body.username,
password: req.body.password,
email: req.body.email
});
await user.save();
req.session.user_id = user.id;
setLoginCookie(user.username, user.password, res);
res.send(JSON.stringify({ error_code: 1 }));
}
} catch (e) {
syzoj.log(e);
res.send(JSON.stringify({ error_code: e }));
}
});
app.get('/api/sign_up/:token', async (req, res) => {
try {
let obj;
try {
let decrypted = syzoj.utils.decrypt(Buffer.from(req.params.token, 'base64'), syzoj.config.register_mail.key).toString();
obj = JSON.parse(decrypted);
} catch (e) {
throw new ErrorMessage('无效的注册验证链接。');
}
let user = await User.fromName(obj.username);
if (user) throw new ErrorMessage('用户名已被占用。');
user = await User.findOne({ where: { email: obj.email } });
if (user) throw new ErrorMessage('邮件地址已被占用。');
// Because the salt is "syzoj2_xxx" and the "syzoj2_xxx" 's md5 is"59cb..."
// the empty password 's md5 will equal "59cb.."
let syzoj2_xxx_md5 = '59cb65ba6f9ad18de0dcd12d5ae11bd2';
if (obj.password === syzoj2_xxx_md5) throw new ErrorMessage('密码不能为空。');
if (!(obj.email = obj.email.trim())) throw new ErrorMessage('邮件地址不能为空。');
if (!syzoj.utils.isValidUsername(obj.username)) throw new ErrorMessage('用户名不合法。');
user = await User.create({
username: req.body.username,
password: req.body.password,
email: req.body.email
username: obj.username,
password: obj.password,
email: obj.email
});
await user.save();
req.session.user_id = user.id;
setLoginCookie(user.username, user.password, res);
res.send(JSON.stringify({ error_code: 1 }));
res.redirect(obj.prevUrl || '/');
} catch (e) {
syzoj.log(e);
res.send(JSON.stringify({ error_code: e }));
res.render('error', {
err: e
});
}
});
@ -109,18 +179,32 @@ app.get('/api/waiting_judge', async (req, res) => {
});
if (judge_state) {
res.send({
have_task: 1,
judge_id: judge_state.id,
code: judge_state.code,
language: judge_state.language,
testdata: judge_state.problem.testdata ? judge_state.problem.testdata.md5 : '',
time_limit: judge_state.problem.time_limit,
memory_limit: judge_state.problem.memory_limit,
file_io: judge_state.problem.file_io,
file_io_input_name: judge_state.problem.file_io_input_name,
file_io_output_name: judge_state.problem.file_io_output_name
});
await judge_state.loadRelationships();
await judge_state.problem.loadRelationships();
if (judge_state.problem.type === 'submit-answer') {
res.send({
have_task: 1,
judge_id: judge_state.id,
answer_file: judge_state.code,
testdata: judge_state.problem.id,
problem_type: judge_state.problem.type
});
} else {
res.send({
have_task: 1,
judge_id: judge_state.id,
code: judge_state.code,
language: judge_state.language,
testdata: judge_state.problem.id,
time_limit: judge_state.problem.time_limit,
memory_limit: judge_state.problem.memory_limit,
file_io: judge_state.problem.file_io,
file_io_input_name: judge_state.problem.file_io_input_name,
file_io_output_name: judge_state.problem.file_io_output_name,
problem_type: judge_state.problem.type
});
}
} else {
res.send({ have_task: 0 });
}
@ -145,9 +229,9 @@ app.post('/api/update_judge/:id', async (req, res) => {
}
});
app.get('/static/uploads/:md5', async (req, res) => {
app.get('/static/uploads/answer/:md5', async (req, res) => {
try {
res.sendFile(TestData.resolvePath(req.params.md5));
res.sendFile(File.resolvePath('answer', req.params.md5));
} catch (e) {
res.status(500).send(e);
}

15
modules/contest.js

@ -28,8 +28,12 @@ let User = syzoj.model('user');
app.get('/contests', async (req, res) => {
try {
let paginate = syzoj.utils.paginate(await Contest.count(), req.query.page, syzoj.config.page.contest);
let contests = await Contest.query(paginate, null, [['start_time', 'desc']]);
let where;
if (res.locals.user && await 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']]);
await contests.forEachAsync(async x => x.subtitle = await syzoj.utils.markdown(x.subtitle));
@ -85,6 +89,9 @@ app.post('/contest/:id/edit', async (req, res) => {
let ranklist = await ContestRanklist.create();
await ranklist.save();
contest.ranklist_id = ranklist.id;
// Only new contest can be set type
contest.type = req.body.type;
}
if (!req.body.title.trim()) throw new ErrorMessage('比赛名不能为空。');
@ -93,10 +100,10 @@ app.post('/contest/:id/edit', async (req, res) => {
if (!Array.isArray(req.body.problems)) req.body.problems = [req.body.problems];
contest.problems = req.body.problems.join('|');
if (!['noi', 'ioi', 'acm'].includes(req.body.type)) throw new ErrorMessage('无效的赛制。');
contest.type = req.body.type;
contest.information = req.body.information;
contest.start_time = syzoj.utils.parseDate(req.body.start_time);
contest.end_time = syzoj.utils.parseDate(req.body.end_time);
contest.is_public = req.body.is_public === 'on';
await contest.save();
@ -334,6 +341,8 @@ app.get('/contest/:id/:pid', async (req, res) => {
let problem_id = problems_id[pid - 1];
let problem = await Problem.fromID(problem_id);
problem.specialJudge = await problem.hasSpecialJudge();
await syzoj.utils.markdown(problem, [ 'description', 'input_format', 'output_format', 'example', 'limit_and_hint' ]);
let state = await problem.getJudgeState(res.locals.user, false);

3
modules/discussion.js

@ -57,7 +57,7 @@ app.get('/article/:id', async (req, res) => {
let paginate = syzoj.utils.paginate(await ArticleComment.count(where), req.query.page, syzoj.config.page.article_comment);
let comments = await ArticleComment.query(paginate, where, [['public_time', 'asc']]);
let comments = await ArticleComment.query(paginate, where, [['public_time', 'desc']]);
for (let comment of comments) {
comment.content = await syzoj.utils.markdown(comment.content);
@ -124,6 +124,7 @@ app.post('/article/:id/edit', async (req, res) => {
article.title = req.body.title;
article.content = req.body.content;
article.update_time = time;
article.is_notice = res.locals.user && res.locals.user.is_admin && req.body.is_notice === 'on';
await article.save();

22
modules/index.js

@ -29,25 +29,21 @@ app.get('/', async (req, res) => {
let ranklist = await User.query([1, 10], { is_show: true }, [['ac_num', 'desc']]);
await ranklist.forEachAsync(async x => x.renderInformation());
let notices = await syzoj.config.notices.mapAsync(async notice => {
if (notice.type === 'link') return notice;
else if (notice.type === 'article') {
let article = await Article.fromID(notice.id);
if (!article) throw new ErrorMessage(`无此帖子:${notice.id}`);
return {
title: article.title,
url: syzoj.utils.makeUrl(['article', article.id]),
date: syzoj.utils.formatDate(article.public_time, 'L')
};
}
});
let notices = (await Article.query(null, { is_notice: true }, [['public_time', 'desc']])).map(article => ({
title: article.title,
url: syzoj.utils.makeUrl(['article', article.id]),
date: syzoj.utils.formatDate(article.public_time, 'L')
}));
let fortune = null;
if (res.locals.user) {
fortune = Divine(res.locals.user.username, res.locals.user.sex);
}
let contests = await Contest.query([1, 5], null, [['start_time', 'desc']]);
let where;
if (res.locals.user && await res.locals.user.is_admin) where = {}
else where = { is_public: true };
let contests = await Contest.query([1, 5], where, [['start_time', 'desc']]);
let hitokoto;
try {

185
modules/problem.js

@ -190,6 +190,7 @@ app.get('/problem/:id', async (req, res) => {
problem.allowedEdit = await problem.isAllowedEditBy(res.locals.user);
problem.allowedManage = await problem.isAllowedManageBy(res.locals.user);
problem.specialJudge = await problem.hasSpecialJudge();
if (problem.is_public || problem.allowedEdit) {
await syzoj.utils.markdown(problem, [ 'description', 'input_format', 'output_format', 'example', 'limit_and_hint' ]);
@ -427,7 +428,7 @@ app.post('/problem/:id/import', async (req, res) => {
let fs = require('bluebird').promisifyAll(require('fs'));
try {
let data = await download(req.body.url + (req.body.url.endsWith('/') ? 'download' : '/download'));
let data = await download(req.body.url + (req.body.url.endsWith('/') ? 'testdata/download' : '/testdata/download'));
await fs.writeFileAsync(tmpFile.path, data);
await problem.updateTestdata(tmpFile.path);
} catch (e) {
@ -443,7 +444,8 @@ app.post('/problem/:id/import', async (req, res) => {
}
});
app.get('/problem/:id/data', async (req, res) => {
// The 'manage' is not `allow manage`'s 'manage', I just have no better name for it.
app.get('/problem/:id/manage', async (req, res) => {
try {
let id = parseInt(req.params.id);
let problem = await Problem.fromID(id);
@ -453,8 +455,11 @@ app.get('/problem/:id/data', async (req, res) => {
await problem.loadRelationships();
res.render('problem_data', {
problem: problem
let testcases = await syzoj.utils.parseTestdata(problem.getTestdataPath());
res.render('problem_manage', {
problem: problem,
testcases: testcases
});
} catch (e) {
syzoj.log(e);
@ -464,7 +469,7 @@ app.get('/problem/:id/data', async (req, res) => {
}
});
app.post('/problem/:id/data', app.multer.single('testdata'), 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);
@ -480,16 +485,31 @@ app.post('/problem/:id/data', app.multer.single('testdata'), async (req, res) =>
problem.file_io_input_name = req.body.file_io_input_name;
problem.file_io_output_name = req.body.file_io_output_name;
if (req.body.type !== 'traditional') {
throw new ErrorMessage('暂不支持该题目类型。');
}
if (problem.type === 'submit-answer' && req.body.type !== 'submit-answer' || problem.type !== 'submit-answer' && req.body.type === 'submit-answer') {
if (await JudgeState.count({ problem_id: id }) !== 0) {
throw new ErrorMessage('已有提交的题目不允许在提交答案和非提交答案之间更改。');
}
}
problem.type = req.body.type;
let validateMsg = await problem.validate();
if (validateMsg) throw new ErrorMessage('无效的题目数据配置。', null, validateMsg);
if (req.file) {
await problem.updateTestdata(req.file.path);
if (req.files['testdata']) {
await problem.updateTestdata(req.files['testdata'][0].path);
}
if (req.files['additional_file']) {
await problem.updateFile(req.files['additional_file'][0].path, 'additional_file');
}
await problem.save();
res.redirect(syzoj.utils.makeUrl(['problem', id, 'data']));
res.redirect(syzoj.utils.makeUrl(['problem', id, 'manage']));
} catch (e) {
syzoj.log(e);
res.render('error', {
@ -529,21 +549,52 @@ app.get('/problem/:id/dis_public', async (req, res) => {
await setPublic(req, res, false);
});
app.post('/problem/:id/submit', 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);
if (!problem) throw new ErrorMessage('无此题目。');
if (!syzoj.config.languages[req.body.language]) throw new ErrorMessage('不支持该语言。');
if (problem.type !== 'submit-answer' && !syzoj.config.languages[req.body.language]) throw new ErrorMessage('不支持该语言。');
if (!res.locals.user) throw new ErrorMessage('请登录后继续。', { '登录': syzoj.utils.makeUrl(['login'], { 'url': syzoj.utils.makeUrl(['problem', id]) }) });
let judge_state = await JudgeState.create({
code: req.body.code,
language: req.body.language,
user_id: res.locals.user.id,
problem_id: req.params.id
});
let judge_state;
if (problem.type === 'submit-answer') {
if (!req.files['answer']) throw new ErrorMessage('请上传答案文件。');
if (req.files['answer'][0].size > syzoj.config.limit.submit_answer) throw new ErrorMessage('答案文件太大。');
let File = syzoj.model('file');
let file = await File.upload(req.files['answer'][0].path, 'answer');
let size = await file.getUnzipSize();
if (size > syzoj.config.limit.submit_answer) throw new ErrorMessage('答案文件太大。');
if (!file.md5) throw new ErrorMessage('上传答案文件失败。');
judge_state = await JudgeState.create({
code: file.md5,
max_memory: size,
language: '',
user_id: res.locals.user.id,
problem_id: req.params.id
});
} else {
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();
} else {
if (req.body.code.length > syzoj.config.limit.submit_code) throw new ErrorMessage('代码太长。');
code = req.body.code;
}
judge_state = await JudgeState.create({
code: code,
language: req.body.language,
user_id: res.locals.user.id,
problem_id: req.params.id
});
}
let contest_id = parseInt(req.query.contest_id), redirectToContest = false;
if (contest_id) {
@ -579,7 +630,103 @@ app.post('/problem/:id/submit', async (req, res) => {
}
});
app.get('/problem/:id/download', async (req, res) => {
app.get('/problem/:id/testdata', async (req, res) => {
try {
let id = parseInt(req.params.id);
let problem = await Problem.fromID(id);
if (!problem) throw new ErrorMessage('无此题目。');
if (!await problem.isAllowedUseBy(res.locals.user)) throw new ErrorMessage('您没有权限进行此操作。');
let testdata = await problem.listTestdata();
let testcases = await syzoj.utils.parseTestdata(problem.getTestdataPath());
problem.allowedEdit = await problem.isAllowedEditBy(res.locals.user)
res.render('problem_data', {
problem: problem,
testdata: testdata,
testcases: testcases
});
} catch (e) {
syzoj.log(e);
res.status(404);
res.render('error', {
err: e
});
}
});
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);
if (!problem) throw new ErrorMessage('无此题目。');
if (!await problem.isAllowedEditBy(res.locals.user)) throw new ErrorMessage('您没有权限进行此操作。');
if (req.files) {
for (let file of req.files) {
await problem.uploadTestdataSingleFile(file.originalname, file.path, file.size);
}
}
res.redirect(syzoj.utils.makeUrl(['problem', id, 'testdata']));
} catch (e) {
syzoj.log(e);
res.render('error', {
err: e
});
}
});
app.get('/problem/:id/testdata/delete/:filename', async (req, res) => {
try {
let id = parseInt(req.params.id);
let problem = await Problem.fromID(id);
if (!problem) throw new ErrorMessage('无此题目。');
if (!await problem.isAllowedEditBy(res.locals.user)) throw new ErrorMessage('您没有权限进行此操作。');
await problem.deleteTestdataSingleFile(req.params.filename);
res.redirect(syzoj.utils.makeUrl(['problem', id, 'testdata']));
} catch (e) {
syzoj.log(e);
res.render('error', {
err: e
});
}
});
app.get('/problem/:id/testdata/download/:filename?', async (req, res) => {
try {
let id = parseInt(req.params.id);
let problem = await Problem.fromID(id);
if (!problem) throw new ErrorMessage('无此题目。');
if (!await problem.isAllowedUseBy(res.locals.user)) throw new ErrorMessage('您没有权限进行此操作。');
if (!req.params.filename) {
if (!await syzoj.utils.isFile(problem.getTestdataPath() + '.zip')) {
await problem.makeTestdataZip();
}
}
let path = require('path');
let filename = req.params.filename ? path.join(problem.getTestdataPath(), req.params.filename) : (problem.getTestdataPath() + '.zip');
if (!await syzoj.utils.isFile(filename)) throw new ErrorMessage('文件不存在。');
res.download(filename, path.basename(filename));
} catch (e) {
syzoj.log(e);
res.status(404);
res.render('error', {
err: e
});
}
});
app.get('/problem/:id/download/additional_file', async (req, res) => {
try {
let id = parseInt(req.params.id);
let problem = await Problem.fromID(id);
@ -589,9 +736,9 @@ app.get('/problem/:id/download', async (req, res) => {
await problem.loadRelationships();
if (!problem.testdata) throw new ErrorMessage('无测试数据。');
if (!problem.additional_file) throw new ErrorMessage('无附加文件。');
res.download(problem.testdata.getPath(), `testdata_${id}.zip`);
res.download(problem.additional_file.getPath(), `additional_file_${id}.zip`);
} catch (e) {
syzoj.log(e);
res.status(404);

17
modules/submission.js

@ -42,7 +42,10 @@ app.get('/submissions', async (req, res) => {
}
};
if (req.query.language) where.language = req.query.language;
if (req.query.language) {
if (req.query.language === 'submit-answer') where.language = '';
else where.language = req.query.language;
}
if (req.query.status) where.status = { $like: req.query.status + '%' };
where.type = { $ne: 1 };
@ -136,8 +139,10 @@ app.get('/submission/:id', async (req, res) => {
await judge.loadRelationships();
judge.codeLength = judge.code.length;
judge.code = await syzoj.utils.highlight(judge.code, syzoj.config.languages[judge.language].highlight);
if (judge.problem.type !== 'submit-answer') {
judge.codeLength = judge.code.length;
judge.code = await syzoj.utils.highlight(judge.code, syzoj.config.languages[judge.language].highlight);
}
judge.allowedSeeCode = await judge.isAllowedSeeCodeBy(res.locals.user);
judge.allowedSeeCase = await judge.isAllowedSeeCaseBy(res.locals.user);
judge.allowedSeeData = await judge.isAllowedSeeDataBy(res.locals.user);
@ -181,8 +186,10 @@ app.get('/submission/:id/ajax', async (req, res) => {
await judge.loadRelationships();
judge.codeLength = judge.code.length;
judge.code = await syzoj.utils.highlight(judge.code, syzoj.config.languages[judge.language].highlight);
if (judge.problem.type !== 'submit-answer') {
judge.codeLength = judge.code.length;
judge.code = await syzoj.utils.highlight(judge.code, syzoj.config.languages[judge.language].highlight);
}
judge.allowedSeeCode = await judge.isAllowedSeeCodeBy(res.locals.user);
judge.allowedSeeCase = await judge.isAllowedSeeCaseBy(res.locals.user);
judge.allowedSeeData = await judge.isAllowedSeeDataBy(res.locals.user);

1
modules/user.js

@ -181,7 +181,6 @@ app.post('/user/:id/edit', async (req, res) => {
error_info: ''
});
} catch (e) {
console.log(e);
user.privileges = await user.getPrivileges();
res.locals.user.allowedManage = await res.locals.user.hasPrivilege('manage_user');

3
package.json

@ -4,7 +4,7 @@
"description": "An OnlineJudge System for OI",
"main": "app.js",
"scripts": {
"start": "node --harmony-async-await app.js",
"start": "node app.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
@ -44,6 +44,7 @@
"pygmentize-bundled-cached": "^1.1.0",
"request": "^2.74.0",
"request-promise": "^4.1.1",
"sendmail": "^1.1.1",
"sequelize": "^3.24.3",
"session-file-store": "^1.0.0",
"sqlite3": "^3.1.4",

4
static/style.css

@ -349,6 +349,10 @@ table.center.aligned ul, table.center.aligned ol {
text-align: left;
}
body > .ui.page.dimmer {
position: fixed !important;
}
/* status color */
/*

0
uploads/testdata/.placeholder → uploads/additional_file/.gitkeep

0
uploads/answer/.gitkeep

0
uploads/testdata/.gitkeep vendored

166
utility.js

@ -36,7 +36,9 @@ global.ErrorMessage = class ErrorMessage {
}
};
let Promise = require('bluebird');
let path = require('path');
let fs = Promise.promisifyAll(require('fs-extra'));
let util = require('util');
let renderer = require('moemark-renderer');
let moment = require('moment');
@ -44,7 +46,6 @@ let url = require('url');
let querystring = require('querystring');
let pygmentize = require('pygmentize-bundled-cached');
let gravatar = require('gravatar');
let AdmZip = require('adm-zip');
let filesize = require('file-size');
let AsyncLock = require('async-lock');
@ -195,82 +196,89 @@ module.exports = {
gravatar(email, size) {
return gravatar.url(email, { s: size, d: 'mm' }).replace('www', 'cn');
},
parseTestData(filename) {
let zip = new AdmZip(filename);
let list = zip.getEntries().filter(e => !e.isDirectory).map(e => e.entryName);
let res = [];
if (!list.includes('data_rule.txt')) {
res[0] = {};
res[0].cases = [];
for (let file of list) {
let parsedName = path.parse(file);
if (parsedName.ext === '.in') {
if (list.includes(`${parsedName.name}.out`)) {
res[0].cases.push({
input: file,
output: `${parsedName.name}.out`
});
}
async parseTestdata(dir) {
if (!await syzoj.utils.isDir(dir)) return null;
try {
// Get list of *files*
let list = await (await fs.readdirAsync(dir)).filterAsync(async x => await syzoj.utils.isFile(path.join(dir, x)));
let res = [];
if (!list.includes('data_rule.txt')) {
res[0] = {};
res[0].cases = [];
for (let file of list) {
let parsedName = path.parse(file);
if (parsedName.ext === '.in') {
if (list.includes(`${parsedName.name}.out`)) {
res[0].cases.push({
input: file,
output: `${parsedName.name}.out`
});
}
if (list.includes(`${parsedName.name}.ans`)) {
res[0].cases.push({
input: file,
output: `${parsedName.name}.ans`
});
if (list.includes(`${parsedName.name}.ans`)) {
res[0].cases.push({
input: file,
output: `${parsedName.name}.ans`
});
}
}
}
}
res[0].type = 'sum';
res[0].score = 100;
res[0].cases.sort((a, b) => {
function getLastInteger(s) {
let re = /(\d+)\D*$/;
let x = re.exec(s);
if (x) return parseInt(x[1]);
else return -1;
}
res[0].type = 'sum';
res[0].score = 100;
res[0].cases.sort((a, b) => {
function getLastInteger(s) {
let re = /(\d+)\D*$/;
let x = re.exec(s);
if (x) return parseInt(x[1]);
else return -1;
}
return getLastInteger(a.input) - getLastInteger(b.input);
});
} else {
let lines = zip.readAsText('data_rule.txt').split('\r').join('').split('\n').filter(x => x.length !== 0);
return getLastInteger(a.input) - getLastInteger(b.input);
});
} else {
let lines = (await fs.readFileAsync(path.join(dir, 'data_rule.txt'))).toString().split('\r').join('').split('\n').filter(x => x.length !== 0);
if (lines.length < 3) throw '无效的数据配置文件(data_rule.txt)。';
if (lines.length < 3) throw '无效的数据配置文件(data_rule.txt)。';
let input = lines[lines.length - 2];
let output = lines[lines.length - 1];
let input = lines[lines.length - 2];
let output = lines[lines.length - 1];
for (let s = 0; s < lines.length - 2; ++s) {
res[s] = {};
res[s].cases = [];
let numbers = lines[s].split(' ').filter(x => x);
if (numbers[0].includes(':')) {
let tokens = numbers[0].split(':');
res[s].type = tokens[0] || 'sum';
res[s].score = parseFloat(tokens[1]) || (100 / (lines.length - 2));
numbers.shift();
} else {
res[s].type = 'sum';
res[s].score = 100;
}
for (let i of numbers) {
let testcase = {
input: input.replace('#', i),
output: output.replace('#', i)
};
for (let s = 0; s < lines.length - 2; ++s) {
res[s] = {};
res[s].cases = [];
let numbers = lines[s].split(' ').filter(x => x);
if (numbers[0].includes(':')) {
let tokens = numbers[0].split(':');
res[s].type = tokens[0] || 'sum';
res[s].score = parseFloat(tokens[1]) || (100 / (lines.length - 2));
numbers.shift();
} else {
res[s].type = 'sum';
res[s].score = 100;
}
for (let i of numbers) {
let testcase = {
input: input.replace('#', i),
output: output.replace('#', i)
};
if (!list.includes(testcase.input)) throw `找不到文件 ${testcase.input}`;
if (!list.includes(testcase.output)) throw `找不到文件 ${testcase.output}`;
res[s].cases.push(testcase);
if (!list.includes(testcase.input)) throw `找不到文件 ${testcase.input}`;
if (!list.includes(testcase.output)) throw `找不到文件 ${testcase.output}`;
res[s].cases.push(testcase);
}
}
res = res.filter(x => x.cases && x.cases.length !== 0);
}
res = res.filter(x => x.cases && x.cases.length !== 0);
res.spj = list.includes('spj.js') || list.some(s => s.startsWith('spj_'));
return res;
} catch (e) {
return { error: e };
}
res.spj = list.includes('spj.js') || list.some(s => s.startsWith('spj_'));
return res;
},
ansiToHTML(s) {
let Convert = require('ansi-to-html');
@ -307,11 +315,10 @@ module.exports = {
try {
let request = require('request-promise');
let res = await request({
uri: 'http://api.hitokoto.us/rand',
uri: 'https://sslapi.hitokoto.cn',
timeout: 1500,
qs: {
encode: 'json',
cat: 'a'
c: 'a'
},
json: true
});
@ -329,5 +336,30 @@ module.exports = {
let s = JSON.stringify(key);
if (!this.locks[s]) this.locks[s] = new AsyncLock();
return this.locks[s].acquire(s, cb);
},
encrypt(buffer, password) {
if (typeof buffer === 'string') buffer = Buffer.from(buffer);
let crypto = require('crypto');
let cipher = crypto.createCipher('aes-256-ctr', password);
return Buffer.concat([cipher.update(buffer), cipher.final()]);
},
decrypt(buffer, password) {
let crypto = require('crypto');
let decipher = crypto.createDecipher('aes-256-ctr', password);
return Buffer.concat([decipher.update(buffer), decipher.final()]);
},
async isFile(path) {
try {
return (await fs.statAsync(path)).isFile();
} catch (e) {
return false;
}
},
async isDir(path) {
try {
return (await fs.statAsync(path)).isDirectory();
} catch (e) {
return false;
}
}
};

48
views/article.ejs

@ -1,17 +1,36 @@
<% this.title = article.title + ' - 帖子'; %>
<% include header %>
<style type="text/css" xmlns:style="http://www.w3.org/1999/xhtml">
.small{
font-size: 0.7em;
}
.small{
font-size: 0.7em;
}
</style>
<div class="padding">
<h1><%= article.title %></h1>
<p style="font_size: 0.7em"><img style="vertical-align: middle;" src="<%= syzoj.utils.gravatar(article.user.email, 64) %>" width="32" height="32">
<a href="<%= syzoj.utils.makeUrl(['user', article.user_id]) %>"><%= article.user.username %></a><% if (article.user.nameplate) { %><%- article.user.nameplate %><% } %> 于 <%= syzoj.utils.formatDate(article.public_time) %> 发表,<%= syzoj.utils.formatDate(article.update_time) %> 最后更新
<% if (article.allowedEdit) { %>
<a class="ui mini red button" href="<%= syzoj.utils.makeUrl(['article', article.id, 'delete']) %>">删除文章</a>
<a class="ui mini red button" onclick="$('#modal-delete').modal('show')">删除文章</a>
<a class="ui mini button" href="<%= syzoj.utils.makeUrl(['article', article.id, 'edit']) %>">编辑文章</a>
<div class="ui basic modal" id="modal-delete">
<div class="ui icon header">
<i class="trash icon"></i>
<p style="margin-top: 15px; ">删除文章</p>
</div>
<div class="content" style="text-align: center; ">
<p>确认删除这篇文章吗?</p>
</div>
<div class="actions">
<div class="ui red basic cancel inverted button">
<i class="remove icon"></i>
</div>
<a class="ui green ok inverted button" href="<%= syzoj.utils.makeUrl(['article', article.id, 'delete']) %>">
<i class="checkmark icon"></i>
</a>
</div>
</div>
<% } %>
</p>
<div class="ui existing segment">
@ -32,7 +51,26 @@
</div>
<div class="text font-content"><%- comment.content %></div>
<% if (comment.allowedEdit) { %>
<div class="actions"><a href="<%= syzoj.utils.makeUrl(['article', article.id, 'comment', comment.id, 'delete']) %>">删除</a></div>
<div class="actions"><a onclick="$('#modal-delete-<%= comment.id %>').modal('show')">删除</a></div>
<div class="ui basic modal" id="modal-delete-<%= comment.id %>">
<div class="ui icon header">
<i class="trash icon"></i>
<p style="margin-top: 15px; ">删除评论</p>
</div>
<div class="content" style="text-align: center; ">
<p>确认删除这条评论吗?</p>
</div>
<div class="actions">
<div class="ui red basic cancel inverted button">
<i class="remove icon"></i>
</div>
<a class="ui green ok inverted button" href="<%= syzoj.utils.makeUrl(['article', article.id, 'comment', comment.id, 'delete']) %>">
<i class="checkmark icon"></i>
</a>
</div>
</div>
<% } %>
</div>
</div>

7
views/article_edit.ejs

@ -17,6 +17,13 @@
<input type="text" id="title" name="title" value="<%= article.title %>">
<label for="content">内容</label>
<textarea rows="15" id="content" name="content" class="font-content"><%= article.content %></textarea>
<% if (user && user.is_admin) { %>
<div class="ui <% if (article.is_notice) { %>checked <% } %>checkbox" style="margin-top: 15px; ">
<input <% if (article.is_notice) { %>checked=""<% } %> name="is_notice" type="checkbox">
<label><strong>公告</strong></label>
<p style="margin-top: 5px; ">选择后将显示在首页公告栏。</p>
</div>
<% } %>
</div>
</div>
<div class="ui bottom attached tab segment" data-tab="preview">

13
views/contest_edit.ejs

@ -22,19 +22,19 @@
<label>赛制</label>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="type" id="type-noi" value="noi"<% if (contest.type === 'noi') { %> checked="checked"<% } %>>
<input <% if (contest.id) { %>disabled <% } %>type="radio" name="type" id="type-noi" value="noi"<% if (contest.type === 'noi') { %> checked="checked"<% } %>>
<label for="type-noi">NOI</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="type" id="type-ioi" value="ioi"<% if (contest.type === 'ioi') { %> checked="checked"<% } %>>
<input <% if (contest.id) { %>disabled <% } %>type="radio" name="type" id="type-ioi" value="ioi"<% if (contest.type === 'ioi') { %> checked="checked"<% } %>>
<label for="type-ioi">IOI</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="type" id="type-acm" value="acm"<% if (contest.type === 'acm') { %> checked="checked"<% } %>>
<input <% if (contest.id) { %>disabled <% } %>type="radio" name="type" id="type-acm" value="acm"<% if (contest.type === 'acm') { %> checked="checked"<% } %>>
<label for="type-acm">ACM</label>
</div>
</div>
@ -51,6 +51,13 @@
<label>结束时间</label>
<input type="text" name="end_time" value="<%= syzoj.utils.formatDate(contest.end_time || syzoj.utils.getCurrentDate()) %>">
</div>
<div class="inline field">
<label class="ui header">公开</label>
<div class="ui toggle checkbox">
<input type="checkbox"<% if (contest.is_public) { %> checked<% } %> name="is_public">
<label><span style="visibility: hidden; "> </span></label>
</div>
</div>
<button type="submit" class="ui button">提交</button>
</form>
<script>

10
views/index.ejs

@ -80,7 +80,7 @@
<h4 class="ui top attached block header">一言(ヒトコト)</h4>
<div class="ui bottom attached center aligned segment">
<div style="font-size: 1em; line-height: 1.5em; "><%= hitokoto.hitokoto %></div>
<% if (hitokoto.source) { %><div style="text-align: right; margin-top: 15px; font-size: 0.9em; color: #666; ">——<%= hitokoto.source %></div><% } %>
<% if (hitokoto.from) { %><div style="text-align: right; margin-top: 15px; font-size: 0.9em; color: #666; ">——<%= hitokoto.from %></div><% } %>
</div>
<% } %>
<%
@ -182,14 +182,6 @@
</table>
<% } %>
</div>
<h4 class="ui top attached block header font-content">信息栏</h4>
<div class="ui bottom attached <% if (!syzoj.config.announcement) { %>center aligned <% } %>segment">
<% if (!syzoj.config.announcement) { %>
无任何信息
<% } else { %>
<p><%- syzoj.config.announcement %></p>
<% } %>
</div>
<% if (typeof links !== 'undefined' && links) { %>
<h4 class="ui top attached block header font-content">友情链接</h4>
<div class="ui bottom attached segment">

140
views/problem.ejs

@ -52,6 +52,10 @@ if (contest) {
<span class="ui label">标准输入输出</span>
<% } %>
</div>
<div class="row" style="margin-top: -23px">
<span class="ui label">评测方式:<%= problem.specialJudge ? 'Special Judge' : '文本比较' %></span>
<span class="ui label">题目类型:<%= { 'submit-answer': '答案提交', 'interaction': '交互', 'traditional': '传统' }[problem.type] %></span>
</div>
<div class="row" style="margin-top: -23px">
<span class="ui label">上传者:
<% if (problem.is_anonymous && !problem.allowedManage) { %>
@ -88,14 +92,15 @@ if (contest) {
<a class="small ui primary button" href="#submit_code">提交</a>
<a class="small ui positive button" href="<%= syzoj.utils.makeUrl(['submissions'], { problem_id: problem.id }) %>">提交记录</a>
<a class="small ui orange button" href="<%= syzoj.utils.makeUrl(['problem', problem.id, 'statistics', 'fastest']) %>">统计</a>
<a class="small ui yellow button" href="<%= syzoj.utils.makeUrl(['problem', problem.id, 'download']) %>">下载测试数据</a>
<a class="small ui yellow button" href="<%= syzoj.utils.makeUrl(['problem', problem.id, 'testdata']) %>">测试数据</a>
<% } %>
<% if (problem.additional_file) { %><a class="small ui teal button" href="<%= syzoj.utils.makeUrl(['problem', problem.id, 'download', 'additional_file']) %>">附加文件</a><% } %>
</div>
<% if (!contest) { %>
<div class="ui buttons right floated">
<% if (problem.allowedEdit) { %>
<a class="small ui button" href="<%= syzoj.utils.makeUrl(['problem', problem.id, 'edit']) %>">编辑</a>
<a class="small ui button" href="<%= syzoj.utils.makeUrl(['problem', problem.id, 'data']) %>">管理测试数据</a>
<a class="small ui button" href="<%= syzoj.utils.makeUrl(['problem', problem.id, 'manage']) %>">管理</a>
<% } %>
<% if (problem.allowedManage) { %>
<% if (problem.is_public) { %>
@ -187,73 +192,96 @@ if (contest) {
if (contest) formUrl = syzoj.utils.makeUrl(['problem', problem.id, 'submit'], { contest_id: contest.id });
else formUrl = syzoj.utils.makeUrl(['problem', problem.id, 'submit']);
%>
<form class="ui form" action="<%= formUrl %>" method="post" onsubmit="return submit_code()" id="submit_code">
<input name="language" type="hidden" id="form">
<input name="code" type="hidden">
<div class="ui grid">
<div class="four wide column" style="margin-right: -25px; ">
<div class="ui attached vertical fluid pointing menu" id="languages-menu" style="max-height: 370px; overflow-y: scroll; overflow-x: hidden; ">
<%
let language = Object.getOwnPropertyNames(syzoj.config.languages).shift();
if (state) {
language = state.language;
} else if (lastLanguage) language = lastLanguage;
%>
<% for (lang in syzoj.config.languages) { %>
<a class="item<%= lang === language ? ' active' : '' %>" data-value="<%= lang %>" data-mode="<%= syzoj.config.languages[lang].editor %>">
<%= syzoj.config.languages[lang].show %>
<div class="ui right floated" style="opacity: 0.4; margin-top: 8px; font-size: 0.7em; "><%= syzoj.config.languages[lang].version %></div>
</a>
<% } %>
<form class="ui form" action="<%= formUrl %>" method="post" onsubmit="return submit_code()" id="submit_code" enctype="multipart/form-data">
<% if (problem.type === 'submit-answer') { %>
<div class="inline fields">
<div class="field" style="margin: 0 auto; ">
<label for="answer">上传答案(请使用 ZIP 格式压缩)</label>
<input type="file" id="answer" name="answer">
</div>
</div>
<% } else { %>
<input name="language" type="hidden" id="form">
<input name="code" type="hidden">
<div class="ui grid">
<div class="four wide column" style="margin-right: -25px; ">
<div class="ui attached vertical fluid pointing menu" id="languages-menu" style="max-height: 370px; overflow-y: scroll; overflow-x: hidden; ">
<%
let language = Object.getOwnPropertyNames(syzoj.config.languages).shift();
if (state) {
language = state.language;
} else if (lastLanguage) language = lastLanguage;
%>
<% for (lang in syzoj.config.languages) { %>
<a class="item<%= lang === language ? ' active' : '' %>" data-value="<%= lang %>" data-mode="<%= syzoj.config.languages[lang].editor %>">
<%= syzoj.config.languages[lang].show %>
<div class="ui right floated" style="opacity: 0.4; margin-top: 8px; font-size: 0.7em; "><%= syzoj.config.languages[lang].version %></div>
</a>
<% } %>
</div>
</div>
<div class="twelve wide stretched column" style="padding-left: 0; margin-left: calc(-1rem - 1px); width: calc(75% + 1rem + 1px + 25px) !important; ">
<div id="editor" style="border: 1px solid #D4D4D5; "><% if (state) { %><%= state.code %><% } %></div>
</div>
<div class="inline fields" style="width: 100%; ">
<div class="field" style="margin: 0 auto; ">
<label for="answer">或者,上传代码文件</label>
<input type="file" id="answer" name="answer">
</div>
</div>
<div class="twelve wide stretched column" style="padding-left: 0; margin-left: calc(-1rem - 1px); width: calc(75% + 1rem + 1px + 25px) !important; ">
<div id="editor" style="border: 1px solid #D4D4D5; "><% if (state) { %><%= state.code %><% } %></div>
</div>
</div>
<% } %>
<div class="ui center aligned vertical segment" style="padding-bottom: 0; "><button type="submit" class="ui button">提交</button></div>
</form>
</form>
</div>
</div>
<% } %>
</div>
<script src="/libs/ace/ace.js"></script>
<script type="text/javascript">
var editor = ace.edit("editor");
var lastSubmitted = '';
<% if (problem.type !== 'submit-answer') { %>
<script src="/libs/ace/ace.js"></script>
<script type="text/javascript">
var editor = ace.edit("editor");
var lastSubmitted = '';
editor.setTheme("ace/theme/tomorrow");
editor.getSession().setMode("ace/mode/" + $('#languages-menu .item.active').data('mode'));
editor.getSession().setUseSoftTabs(false);
editor.setTheme("ace/theme/tomorrow");
editor.getSession().setMode("ace/mode/" + $('#languages-menu .item.active').data('mode'));
editor.getSession().setUseSoftTabs(false);
editor.container.style.lineHeight = 1.6;
editor.container.style.fontSize = '14px';
editor.container.style.fontFamily = "'Roboto Mono', 'Bitstream Vera Sans Mono', 'Menlo', 'Consolas', 'Lucida Console', monospace";
editor.setShowPrintMargin(false);
editor.renderer.updateFontSize();
editor.container.style.lineHeight = 1.6;
editor.container.style.fontSize = '14px';
editor.container.style.fontFamily = "'Roboto Mono', 'Bitstream Vera Sans Mono', 'Menlo', 'Consolas', 'Lucida Console', monospace";
editor.setShowPrintMargin(false);
editor.renderer.updateFontSize();
function submit_code() {
if (!editor.getValue().trim() || editor.getValue().trim() == lastSubmitted) return false;
$('#submit_code input[name=language]').val($('#languages-menu .item.active').data('value'));
lastSubmitted = editor.getValue();
$('#submit_code input[name=code]').val(lastSubmitted);
return true;
}
function submit_code() {
if (!editor.getValue().trim()) return false;
$('#submit_code input[name=language]').val($('#languages-menu .item.active').data('value'));
lastSubmitted = editor.getValue();
$('#submit_code input[name=code]').val(editor.getValue());
return true;
}
$('#languages-menu')[0].scrollTop = $('#languages-menu .active')[0].offsetTop - $('#languages-menu')[0].firstElementChild.offsetTop;
$('#languages-menu')[0].scrollTop = $('#languages-menu .active')[0].offsetTop - $('#languages-menu')[0].firstElementChild.offsetTop;
$(function () {
$('#languages-menu .item').click(function() {
$(this)
.addClass('active')
.closest('.ui.menu')
.find('.item')
.not($(this))
.removeClass('active')
;
editor.getSession().setMode("ace/mode/" + $(this).data('mode'));
$(function () {
$('#languages-menu .item').click(function() {
$(this)
.addClass('active')
.closest('.ui.menu')
.find('.item')
.not($(this))
.removeClass('active')
;
editor.getSession().setMode("ace/mode/" + $(this).data('mode'));
});
});
});
</script>
</script>
<% } else { %>
<script>
function submit_code() {
if ($('#answer')[0].files.length === 0) return false;
}
</script>
<% } %>
<% include footer %>

289
views/problem_data.ejs

@ -1,151 +1,164 @@
<% this.title = '上传测试数据'; %>
<% include header %>
<%
let subtaskType = {
sum: '测试点分数按百分比相加',
min: '取各测试点最低分',
mul: '测试点分数按百分比相乘'
};
this.title = '测试数据';
function getIcon(filename) {
let a = {
'.cpp': 'file code outline',
'.c': 'file code outline',
'.cs': 'file code outline',
'.pas': 'file code outline',
'.py': 'file code outline',
'.js': 'file code outline',
'.java': 'file code outline',
'.hs': 'file code outline',
'.vala': 'file code outline',
'.lua': 'file code outline',
'.rb': 'file code outline',
'.vb': 'file code outline',
'.ml': 'file code outline',
'.in': 'file text outline',
'.out': 'file text outline',
'.ans': 'file text outline',
'.txt': 'file text outline',
'.md': 'file text outline',
'.md': 'file text outline',
'.docx': 'file word outline',
'.odt': 'file word outline',
'.xlsx': 'file excel outline',
'.ods': 'file excel outline',
'.pptx': 'file powerpoint outline',
'.odp': 'file powerpoint outline',
'.zip': 'file archive outline',
'.7z': 'file archive outline',
}
for (let x in a) if (filename.endsWith(x)) return a[x];
return 'file outline';
}
%>
<% include header %>
<div class="padding">
<div class="ui grid">
<div class="row">
<div class="seven wide column">
<% if (problem.testdata) { %>
<%
try {
let list = syzoj.utils.parseTestData(problem.testdata.getPath());
%>
<% if (list.spj) { %>
<p>评测方式:Special Judge</p>
<% } else { %>
<p>评测方式:文本比较</p>
<% } %>
<table class="ui celled table">
<tbody>
<% let i = 0; %>
<% for (let subtask of list) { %>
<% if (list.length !== 1) { %>
<tr>
<td style="background-color: #F9FAFB" colspan="2"><h4 style="margin-bottom: 3px; ">子任务 <%= ++i %></h4><span style="font-weight: normal; "><%= subtaskType[subtask.type] %>,总分值 <%= subtask.score %></span></th>
</tr>
<% } else { %>
<tr>
<td style="background-color: #F9FAFB" colspan="2"><h4 style="margin-bottom: 3px; ">单个子任务</h4><span style="font-weight: normal; "><%= subtaskType[subtask.type] %></span></th>
</tr>
<% } %>
<% for (let testcase of subtask.cases) { %>
<tr>
<td><%= testcase.input %></td>
<td><%= testcase.output %></td>
</tr>
<% } %>
<% } %>
</tbody>
</table>
<% } catch (e) { %>
<h3>数据包错误:<%= e %></h3>
<div class="ui grid">
<div class="seven wide column">
<h3 style="text-align: center; ">测试点信息</h3>
<% include problem_testcases %>
</div>
<div class="nine wide column">
<h3 style="text-align: center; ">文件列表</h3>
<% if (testdata) { %>
<table class="ui very basic center aligned table">
<thead>
<tr>
<th class="left aligned">文件名</th>
<th style="width: 100px">文件大小</th>
<th style="width: 35px">下载</th>
<% if (problem.allowedEdit) { %>
<th style="width: 35px">删除</th>
<% } %>
</tr>
</thead>
<tbody>
<% if (testdata.zip) { %>
<tr>
<td class="left aligned"><i class="file archive outline icon"></i> 完整数据包</td>
<td><%- !testdata.zip.size ? '<i class="minus icon"></i>' : syzoj.utils.formatSize(testdata.zip.size) %></td>
<td><a style="color: #000; " href="<%= syzoj.utils.makeUrl(['problem', problem.id, 'testdata', 'download']) %>"><i class="download icon"></i></td>
<% if (problem.allowedEdit) { %>
<td><i class="minus icon"></i></td>
<% } %>
</tr>
<% } %>
<% } else { %>
<h3>数据未上传</h3>
<% } %>
</div>
<div class="nine wide column">
<form class="ui form" method="post" enctype="multipart/form-data">
<div class="two fields">
<div class="field">
<label for="doc-ds-ipt-1">时间限制(单位: ms)</label>
<input type="number" name="time_limit" value="<%= problem.time_limit %>">
</div>
<div class="field">
<label for="doc-ds-ipt-1">内存限制(单位: MiB)</label>
<input type="number" name="memory_limit" value="<%= problem.memory_limit %>">
</div>
</div>
<% if (!problem.file_io) { %>
<% let i = 0; %>
<% if (testdata.files) for (let file of testdata.files) { %>
<% i++; %>
<tr>
<td class="left aligned"><i class="<%= getIcon(file.filename) %> icon"></i> <%= file.filename %></td>
<td><%= syzoj.utils.formatSize(file.size) %></td>
<td>
<a style="color: #000; " href="<%= syzoj.utils.makeUrl(['problem', problem.id, 'testdata', 'download', file.filename]) %>">
<i class="download icon"></i>
</a>
</td>
<% if (problem.allowedEdit) { %>
<td>
<a style="color: #000; " onclick="$('#modal-delete-<%= i %>').modal('show')">
<i class="remove icon"></i>
</a>
<div class="ui basic modal" id="modal-delete-<%= i %>">
<div class="ui icon header">
<i class="trash icon"></i>
<p style="margin-top: 15px; ">删除文件</p>
</div>
<div class="content" style="text-align: center; ">
<p>确认删除「 <samp><%= file.filename %></samp> 」吗?</p>
</div>
<div class="actions">
<div class="ui red basic cancel inverted button">
<i class="remove icon"></i>
</div>
<a class="ui green ok inverted button" href="<%= syzoj.utils.makeUrl(['problem', problem.id, 'testdata', 'delete', file.filename]) %>">
<i class="checkmark icon"></i>
</a>
</div>
</div>
</td>
<% } %>
</tr>
<% } %>
</tbody>
</table>
<% } else { %>
<h4 style="text-align: center; ">无测试数据</h4>
<% } %>
<% if (problem.allowedEdit) { %>
<form id="form_upload" class="ui form" action="<%= syzoj.utils.makeUrl(['problem', problem.id, 'testdata', 'upload']) %>" method="post" enctype="multipart/form-data">
<div class="inline fields">
<label>IO 方式</label>
<div class="field">
<div class="ui radio checkbox">
<input name="io_method" value="std-io" id="std-io" type="radio" onclick="goDisable()" checked>
<label for="std-io">标准 IO</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input name="io_method" value="file-io" id="file-io" type="radio" onclick="goEnable()">
<label for="file-io">文件 IO</label>
<div class="field" style="margin: 0 auto; ">
<label for="answer">上传文件(可一次性上传多个)</label>
<input type="file" name="file" multiple id="upload_file">
<div class="ui center aligned vertical segment" style="padding-bottom: 0; ">
<div class="ui button" onclick="check_replace()">提交</div>
<a href="<%= syzoj.utils.makeUrl(['problem', problem.id]) %>" class="ui blue button">返回题目</a>
</div>
</div>
</div>
<div class="two fields">
<div class="field">
<label for="file_io_input_name">输入文件名</label>
<input type="text" id="file-io-input-name" name="file_io_input_name" value="<%= problem.file_io_input_name %>" disabled>
</div>
<div class="field">
<label for="file_io_output_name">输出文件名</label>
<input type="text" id="file-io-output-name" name="file_io_output_name" value="<%= problem.file_io_output_name %>" disabled>
</div>
</form>
<div class="ui basic modal" id="modal-replace">
<div class="ui icon header">
<i class="refresh icon"></i>
<p style="margin-top: 15px; ">替换文件</p>
</div>
<% } else { %>
<div class="inline fields">
<label>IO 方式</label>
<div class="field">
<div class="ui radio checkbox">
<input name="io_method" value="std-io" id="std-io" type="radio" onclick="goDisable()">
<label for="std-io">标准 IO</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input name="io_method" value="file-io" id="file-io" type="radio" onclick="goEnable()" checked>
<label for="file-io">文件 IO</label>
</div>
</div>
<div class="content" style="text-align: center; ">
<p>确认替换以下文件吗?</p>
<div style="display: inline-block; text-align: left; " id="replaced_files"></div>
</div>
<div class="two fields">
<div class="field">
<label for="file_io_input_name">输入文件名</label>
<input type="text" id="file-io-input-name" name="file_io_input_name" value="<%= problem.file_io_input_name %>">
</div>
<div class="field">
<label for="file_io_output_name">输出文件名</label>
<input type="text" id="file-io-output-name" name="file_io_output_name" value="<%= problem.file_io_output_name %>">
<div class="actions">
<div class="ui red basic cancel inverted button">
<i class="remove icon"></i>
</div>
<a class="ui green ok inverted button" onclick="$('#form_upload').submit()">
<i class="checkmark icon"></i>
</a>
</div>
<% } %>
<div class="field">
<label for="testdata"><% if (!problem.testdata_id) { %>上传测试数据<% } else { %>更新测试数据<% } %></label>
<input type="file" id="testdata" name="testdata">
</div>
<button type="submit" class="ui button">提交</button>
<a href="<%= syzoj.utils.makeUrl(['problem', problem.id]) %>" class="ui blue button">返回题目</a>
</form>
</div>
</div>
<script>
function check_replace() {
var old_files = <%- JSON.stringify((testdata && testdata.files ? testdata.files : []).map(x => x.filename)) %>;
var replaced_files = Array.from($('#upload_file')[0].files).map(function (x) { return x.name; }).filter(function (x) { return old_files.includes(x); });
var s = '';
for (let file of replaced_files) s += '<samp>' + file + '</samp><br>';
if (s) {
$('#replaced_files').html(s);
$('#modal-replace').modal('show');
} else {
$('#form_upload').submit();
}
}
</script>
<% } %>
</div>
</div>
<div>
<script>
function goEnable() {
document.getElementById('file-io-input-name').disabled = false;
document.getElementById('file-io-output-name').disabled = false;
}
function goDisable() {
document.getElementById('file-io-input-name').disabled = true;
document.getElementById('file-io-output-name').disabled = true;
}
$(document).ready(function () {
$('#file-io-input-name').on('input keyup change', function (e) {
var prob = $('#file-io-input-name').val();
if (prob.lastIndexOf('.') !== -1) prob = prob.substring(0, prob.lastIndexOf('.'));
$('#file-io-output-name').attr('placeholder', prob + '.out');
});
$('#file-io-output-name').focus(function (e) {
if (!$('#file-io-output-name').val()) {
$('#file-io-output-name').val($('#file-io-output-name').attr('placeholder'));
}
});
});
</script>
<% include footer %>
</div>
<% include footer %>

144
views/problem_manage.ejs

@ -0,0 +1,144 @@
<% this.title = '管理题目'; %>
<% include header %>
<div class="padding">
<div class="ui grid">
<div class="row">
<div class="seven wide column">
<% include problem_testcases %>
</div>
<div class="nine wide column">
<form class="ui form" method="post" enctype="multipart/form-data" onsubmit="return checkSubmit()">
<input type="hidden" name="type" value="<%= problem.type %>">
<div class="ui pointing secondary menu" id="problem-type-tab" style="margin-top: -10px; ">
<a class="<%= problem.type === 'traditional' ? 'active ' : '' %>item" data-tab="traditional">传统</a>
<a class="<%= problem.type === 'interaction' ? 'active ' : '' %>item" data-tab="interaction">交互</a>
<a class="<%= problem.type === 'submit-answer' ? 'active ' : '' %>item" data-tab="submit-answer">提交答案</a>
</div>
<div class="ui <%= problem.type !== 'submit-answer' ? 'active ' : '' %>tab" data-tab="traditional" data-tab="interaction">
<div class="two fields">
<div class="field">
<label for="doc-ds-ipt-1">时间限制(单位: ms)</label>
<input type="number" name="time_limit" value="<%= problem.time_limit %>">
</div>
<div class="field">
<label for="doc-ds-ipt-1">内存限制(单位: MiB)</label>
<input type="number" name="memory_limit" value="<%= problem.memory_limit %>">
</div>
</div>
<% if (!problem.file_io) { %>
<div class="inline fields">
<label>IO 方式</label>
<div class="field">
<div class="ui radio checkbox">
<input name="io_method" value="std-io" id="std-io" type="radio" onclick="goDisable()" checked>
<label for="std-io">标准 IO</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input name="io_method" value="file-io" id="file-io" type="radio" onclick="goEnable()">
<label for="file-io">文件 IO</label>
</div>
</div>
</div>
<div class="two fields">
<div class="field">
<label for="file_io_input_name">输入文件名</label>
<input type="text" id="file-io-input-name" name="file_io_input_name" value="<%= problem.file_io_input_name %>" disabled>
</div>
<div class="field">
<label for="file_io_output_name">输出文件名</label>
<input type="text" id="file-io-output-name" name="file_io_output_name" value="<%= problem.file_io_output_name %>" disabled>
</div>
</div>
<% } else { %>
<div class="inline fields">
<label>IO 方式</label>
<div class="field">
<div class="ui radio checkbox">
<input name="io_method" value="std-io" id="std-io" type="radio" onclick="goDisable()">
<label for="std-io">标准 IO</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input name="io_method" value="file-io" id="file-io" type="radio" onclick="goEnable()" checked>
<label for="file-io">文件 IO</label>
</div>
</div>
</div>
<div class="two fields">
<div class="field">
<label for="file_io_input_name">输入文件名</label>
<input type="text" id="file-io-input-name" name="file_io_input_name" value="<%= problem.file_io_input_name %>">
</div>
<div class="field">
<label for="file_io_output_name">输出文件名</label>
<input type="text" id="file-io-output-name" name="file_io_output_name" value="<%= problem.file_io_output_name %>">
</div>
</div>
<% } %>
</div>
<div class="ui <%= problem.type === 'submit-answer' ? 'active ' : '' %>tab" data-tab="submit-answer" style="margin-bottom: 10px; ">
<b>为了避免系统出错,已有提交的题目不允许在提交答案和非提交答案之间更改。</b><br>
提交答案题目不需要设置时间限制、空间限制以及 IO 方式。<br>
提交时使用的答案文件名需要与测试数据中每个测试点输出文件相同,且后缀名为 <code>.out</code>。
</div>
<div class="field">
<label for="testdata">上传测试数据(请使用 ZIP 格式)</label>
<input type="file" id="testdata" name="testdata">
</div>
<div class="field">
<label for="additional_file">上传附加文件(请使用 ZIP 格式)</label>
<input type="file" id="additional_file" name="additional_file">
</div>
<button type="submit" class="ui button">提交</button>
<a href="<%= syzoj.utils.makeUrl(['problem', problem.id]) %>" class="ui blue button">返回题目</a>
</form>
</div>
</div>
<div>
<script>
function goEnable() {
document.getElementById('file-io-input-name').disabled = false;
document.getElementById('file-io-output-name').disabled = false;
}
function goDisable() {
document.getElementById('file-io-input-name').disabled = true;
document.getElementById('file-io-output-name').disabled = true;
}
$(function () {
$('#file-io-input-name').on('input keyup change', function (e) {
var prob = $('#file-io-input-name').val();
if (prob.lastIndexOf('.') !== -1) prob = prob.substring(0, prob.lastIndexOf('.'));
$('#file-io-output-name').attr('placeholder', prob + '.out');
});
$('#file-io-output-name').focus(function (e) {
if (!$('#file-io-output-name').val()) {
$('#file-io-output-name').val($('#file-io-output-name').attr('placeholder'));
}
});
$('#problem-type-tab .item').tab();
$('a[data-tab="traditional"]').click(function () {
$('input[name=type]').val('traditional');
if ($('div[data-tab="interaction"]').attr('data-tab', 'traditional').length) $('a[data-tab="traditional"]').click();
});
$('a[data-tab="interaction"]').click(function () {
$('input[name=type]').val('interaction');
if ($('div[data-tab="traditional"]').attr('data-tab', 'interaction').length) $('a[data-tab="interaction"]').click();
});
$('a[data-tab="submit-answer"]').click(function () {
$('input[name=type]').val('submit-answer');
});
});
function checkSubmit() {
;
}
</script>
<% include footer %>

42
views/problem_testcases.ejs

@ -0,0 +1,42 @@
<%
let subtaskType = {
sum: '测试点分数按百分比相加',
min: '取各测试点最低分',
mul: '测试点分数按百分比相乘'
};
%>
<% if (testcases && testcases.error) { %>
<h4>数据包错误:<%= testcases.error %></h4>
<%
} else if (testcases) {
%>
<% if (testcases.spj) { %>
<p>评测方式:Special Judge</p>
<% } else { %>
<p>评测方式:文本比较</p>
<% } %>
<table class="ui celled table">
<tbody>
<% let i = 0; %>
<% for (let subtask of testcases) { %>
<% if (testcases.length !== 1) { %>
<tr>
<td style="background-color: #F9FAFB" colspan="2"><h4 style="margin-bottom: 3px; ">子任务 <%= ++i %></h4><span style="font-weight: normal; "><%= subtaskType[subtask.type] %>,总分值 <%= subtask.score %></span></th>
</tr>
<% } else { %>
<tr>
<td style="background-color: #F9FAFB" colspan="2"><h4 style="margin-bottom: 3px; ">单个子任务</h4><span style="font-weight: normal; "><%= subtaskType[subtask.type] %></span></th>
</tr>
<% } %>
<% for (let testcase of subtask.cases) { %>
<tr class="center aligned">
<td style="width: 50%; "><%= testcase.input %></td>
<td style="width: 50%; "><%= testcase.output %></td>
</tr>
<% } %>
<% } %>
</tbody>
</table>
<% } else { %>
<h4 style="text-align: center; ">无测试数据</h4>
<% } %>

27
views/sign_up.ejs

@ -14,10 +14,6 @@
<label for="email">邮箱</label>
<input type="email" placeholder="" id="email">
</div>
<!--div class="field">
<label for="email">邀请码</label>
<input type="text" placeholder="Invitation code" id="invitation_code">
</div-->
<div class="two fields">
<div class="field">
<label class="ui header">密码</label>
@ -37,10 +33,20 @@ function show_error(error) {
$("#error_info").text(error);
$("#error").show();
}
function success() {
alert("注册成功!");
alert("注册成功");
window.location.href = <%- JSON.stringify(req.query.url || '/') %>;
}
function mail_required() {
alert("注册确认邮件已经发送到您的邮箱的垃圾箱,点击邮件内的链接即可完成注册。");
var s = $("#email").val();
var mailWebsite = 'https://mail.' + s.substring(s.indexOf('@') + 1, s.length);
if (mailWebsite === 'https://mail.gmail.com') mailWebsite = 'https://mail.google.com';
window.location.href = mailWebsite;
}
function submit() {
if ($("#password1").val() != $("#password2").val()) {
show_error("两次输入的密码不一致");
@ -55,7 +61,8 @@ function submit() {
data: {
username: $("#username").val(),
password: password,
email: $("#email").val()
email: $("#email").val(),
prevUrl: <%- JSON.stringify(req.query.url || '/') %>
},
success: function(data) {
error_code = data.error_code;
@ -79,11 +86,17 @@ function submit() {
show_error("已经有人用过这个用户名了");
break;
case 2009:
show_error("邀请码错误,请联系管理员索要");
show_error("邮箱地址已被占用");
break;
case 2010:
show_error("验证邮件发送失败");
break;
case 1:
success();
break;
case 2:
mail_required();
break;
default:
show_error("未知错误");
break;

34
views/statistics.ejs

@ -1,11 +1,13 @@
<%
this.title = '统计';
let types = {
fastest: '最快',
slowest: '最慢',
shortest: '最短',
longest: '最长',
earliest: '最早'
fastest: problem.type === 'submit-answer' ? null : '最快',
slowest: problem.type === 'submit-answer' ? null : '最慢',
shortest: problem.type === 'submit-answer' ? null : '最短',
longest: problem.type === 'submit-answer' ? null : '最长',
earliest: '最早',
min: problem.type === 'submit-answer' ? '最小' : '最小内存',
max: problem.type === 'submit-answer' ? '最大' : '最大内存'
};
%>
<% include header %>
@ -38,7 +40,7 @@ function getColorOfScore(score) {
<i class="dropdown icon"></i>
<div class="menu">
<% for (let type in types) { %>
<% if (type !== statistics.type) { %>
<% if (type !== statistics.type && types[type] !== null) { %>
<a class="item" href="<%= syzoj.utils.makeUrl(['problem', problem.id, 'statistics', type]) %>"><%= types[type] %></a>
<% } %>
<% } %>
@ -54,9 +56,13 @@ function getColorOfScore(score) {
<th>题目</th>
<th>状态</th>
<th>分数</th>
<th>总时间</th>
<th>内存</th>
<th>代码</th>
<% if (problem.type !== 'submit-answer') { %>
<th>总时间</th>
<th>内存</th>
<th>代码</th>
<% } else { %>
<th>答案文件</th>
<% } %>
<th>提交者</th>
<th>提交时间</th>
</tr>
@ -74,9 +80,13 @@ function getColorOfScore(score) {
</span>
</a></td>
<td><a href="<%= syzoj.utils.makeUrl(['submission', judge.id]) %>"><span class="score score_<%= parseInt(judge.result.score / 10) || 0 %>"><%= judge.result.score %></span></a></td>
<td><%= judge.result.total_time %> ms</td>
<td><%= parseInt(judge.result.max_memory) || 0 %> K</td>
<td><a href="<%= syzoj.utils.makeUrl(['submission', judge.id]) %>"><%= syzoj.config.languages[judge.language].show %></a> / <%= syzoj.utils.formatSize(judge.code.length) %></td>
<% if (problem.type !== 'submit-answer') { %>
<td><%= judge.result.total_time %> ms</td>
<td><%= parseInt(judge.result.max_memory) || 0 %> K</td>
<td><a href="<%= syzoj.utils.makeUrl(['submission', judge.id]) %>"><%= syzoj.config.languages[judge.language].show %></a> / <%= syzoj.utils.formatSize(judge.code.length) %></td>
<% } else { %>
<td><%= syzoj.utils.formatSize(judge.max_memory) %></td>
<% } %>
<td><a href="<%= syzoj.utils.makeUrl(['user', judge.user_id]) %>"><%= judge.user.username %></a><% if (judge.user.nameplate) { %><%- judge.user.nameplate %><% } %></td>
<td><%= syzoj.utils.formatDate(judge.submit_time) %></td>
</tr>

29
views/submission_content.ejs

@ -1,3 +1,4 @@
<% include util %>
<%
// Sanitize judge results for backward compatibility and clarity
if (!judge.result.subtasks) {
@ -38,9 +39,13 @@ else problemUrl = syzoj.utils.makeUrl(['problem', judge.problem_id]);
<% if ((typeof contest === 'undefined' || !contest) || !((!user || !user.is_admin) && !contest.ended && (contest.type === 'acm' || contest.type === 'noi'))) { %>
<th>分数</th>
<% } %>
<th>总时间</th>
<th>内存</th>
<th>代码</th>
<% if (judge.problem.type !== 'submit-answer') { %>
<th>总时间</th>
<th>内存</th>
<th>代码</th>
<% } else { %>
<th>文件大小</th>
<% } %>
<th>提交者</th>
<th>提交时间</th>
</tr>
@ -56,12 +61,16 @@ else problemUrl = syzoj.utils.makeUrl(['problem', judge.problem_id]);
<% if ((typeof contest === 'undefined' || !contest) || !((!user || !user.is_admin) && !contest.ended && (contest.type === 'acm' || contest.type === 'noi'))) { %>
<td class="score score_<%= parseInt(judge.result.score / 10) || 0 %>"><%= judge.result.score %></td>
<% } %>
<td><%= judge.result.total_time %> ms</td>
<td><%= parseInt(judge.result.max_memory) || 0 %> K</td>
<% if (judge.allowedSeeCode) { %>
<td><%= syzoj.config.languages[judge.language].show %> / <%= syzoj.utils.formatSize(judge.codeLength) %></td>
<% if (judge.problem.type !== 'submit-answer') { %>
<td><%= judge.result.total_time %> ms</td>
<td><%= parseInt(judge.result.max_memory) || 0 %> K</td>
<% if (judge.allowedSeeCode) { %>
<td><%= syzoj.config.languages[judge.language].show %> / <%= syzoj.utils.formatSize(judge.codeLength) %></td>
<% } else { %>
<td><%= syzoj.config.languages[judge.language].show %> / 隐藏 %></td>
<% } %>
<% } else { %>
<td><%= syzoj.config.languages[judge.language].show %> / 隐藏 %></td>
<td><%= syzoj.utils.formatSize(judge.max_memory) %></td>
<% } %>
<td><a href="<%= syzoj.utils.makeUrl(['user', judge.user_id]) %>"><%= judge.user.username %></a><% if (judge.user.nameplate) { %><%- judge.user.nameplate %><% } %></td>
<td><%= syzoj.utils.formatDate(judge.submit_time) %></td>
@ -76,7 +85,7 @@ else problemUrl = syzoj.utils.makeUrl(['problem', judge.problem_id]);
}
window.applyTextFit();
</script>
<% if (judge.allowedSeeCode) { %>
<% if (judge.problem.type !== 'submit-answer' && judge.allowedSeeCode) { %>
<div class="ui existing segment" style="position: relative; ">
<% if (judge.allowedRejudge) { %>
<a id="rejudge-button" href="<%= syzoj.utils.makeUrl(['submission', judge.id, 'rejudge']) %>" class="ui button" style="position: absolute; top: 0px; right: -4px; border-top-left-radius: 0; border-bottom-right-radius: 0; <% if (judge.pending) { %>display: none; <% } %>">重新评测</a>
@ -96,7 +105,7 @@ else problemUrl = syzoj.utils.makeUrl(['problem', judge.problem_id]);
});
</script>
<% } %>
<% if (judge.result.compiler_output && judge.status === 'Compile Error' && judge.allowedSeeCode) { %>
<% if (judge.problem.type !== 'submit-answer' && judge.result.compiler_output && judge.status === 'Compile Error' && judge.allowedSeeCode) { %>
<h3 class="ui header">编译信息</h3>
<div class="ui existing segment"><pre style="margin-top: 0; margin-bottom: 0; "><code><%- syzoj.utils.ansiToHTML(judge.result.compiler_output) %></code></pre></div>
<% } else if (judge.result.spj_compiler_output) { %>

3
views/submissions.ejs

@ -21,6 +21,7 @@
<div class="default text"></div>
<div class="menu">
<div class="item" data-value="">不限</div>
<div class="item" data-value="submit-answer">提交答案</div>
<% for (let lang in syzoj.config.languages) { %>
<div class="item" data-value="<%= lang %>"><%= syzoj.config.languages[lang].show %></div>
<% } %>
@ -70,7 +71,7 @@
<th>分数</th>
<th>总时间</th>
<th>内存</th>
<th>代码</th>
<th>代码 / 答案文件</th>
<th>提交者</th>
<th>提交时间</th>
</tr>

5
views/submissions_item.ejs

@ -19,6 +19,7 @@ textFit(e, { maxFontSize: 14 });
<% if ((typeof contest === 'undefined' || !contest) || !((!user || !user.is_admin) && !contest.ended && (contest.type === 'acm' || contest.type === 'noi'))) { %>
<td><a href="<%= syzoj.utils.makeUrl(['submission', judge.id]) %>"><span class="score score_<%= parseInt(judge.result.score / 10) || 0 %>"><%= judge.result.score %></span></a></td>
<% } %>
<% if (judge.problem.type !== 'submit-answer') { %>
<td><%= judge.result.total_time %> ms</td>
<td><%= parseInt(judge.result.max_memory) || 0 %> K</td>
<% if (judge.allowedSeeCode) { %>
@ -26,6 +27,10 @@ textFit(e, { maxFontSize: 14 });
<% } else { %>
<td><a href="<%= syzoj.utils.makeUrl(['submission', judge.id]) %>"><%= syzoj.config.languages[judge.language].show %></a> / 隐藏</td>
<% } %>
<% } else { %>
<td>-</td><td>-</td>
<td><%= syzoj.utils.formatSize(judge.max_memory) %></td>
<% } %>
<td><a href="<%= syzoj.utils.makeUrl(['user', judge.user_id]) %>"><%= judge.user.username %></a><% if (judge.user.nameplate) { %><%- judge.user.nameplate %><% } %></td>
<td><%= syzoj.utils.formatDate(judge.submit_time) %>
<% if (isPending(judge.status)) { %>

Loading…
Cancel
Save