diff --git a/config-example.json b/config-example.json index 3ed6aa8..b7c4ffc 100644 --- a/config-example.json +++ b/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" } diff --git a/models/article.js b/models/article.js index 898ce41..03b65f5 100644 --- a/models/article.js +++ b/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))); } diff --git a/models/contest.js b/models/contest.js index 8bcfda3..300b00d 100644 --- a/models/contest.js +++ b/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))); } diff --git a/models/file.js b/models/file.js new file mode 100644 index 0000000..2cc7b53 --- /dev/null +++ b/models/file.js @@ -0,0 +1,115 @@ +/* + * This file is part of SYZOJ. + * + * Copyright (c) 2016 Menci + * + * 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 . + */ + +'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; diff --git a/models/judge_state.js b/models/judge_state.js index a3e33b4..eb992c5 100644 --- a/models/judge_state.js +++ b/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; } } diff --git a/models/problem.js b/models/problem.js index 0678c28..5517fe9 100644 --- a/models/problem.js +++ b/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(); } diff --git a/models/testdata.js b/models/testdata.js deleted file mode 100644 index 5da17f9..0000000 --- a/models/testdata.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - * This file is part of SYZOJ. - * - * Copyright (c) 2016 Menci - * - * 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 . - */ - -'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; diff --git a/models/user.js b/models/user.js index 8d9926a..5ddc670 100644 --- a/models/user.js +++ b/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)); diff --git a/modules/api.js b/modules/api.js index 186f074..dc88051 100644 --- a/modules/api.js +++ b/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: `

请点击该链接完成您在 ${syzoj.config.title} 的注册:${url}

如果您不是 ${req.body.username},请忽略此邮件。

` + }); + } 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); } diff --git a/modules/contest.js b/modules/contest.js index 35969fd..2ef4439 100644 --- a/modules/contest.js +++ b/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); diff --git a/modules/discussion.js b/modules/discussion.js index 6756b9a..c9fba57 100644 --- a/modules/discussion.js +++ b/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(); diff --git a/modules/index.js b/modules/index.js index a7eb8ab..388aed0 100644 --- a/modules/index.js +++ b/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 { diff --git a/modules/problem.js b/modules/problem.js index 7db7549..726a4bd 100644 --- a/modules/problem.js +++ b/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); diff --git a/modules/submission.js b/modules/submission.js index 56b86bc..74c7f47 100644 --- a/modules/submission.js +++ b/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); diff --git a/modules/user.js b/modules/user.js index 5492586..5a30c43 100644 --- a/modules/user.js +++ b/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'); diff --git a/package.json b/package.json index b44066a..501d97b 100644 --- a/package.json +++ b/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", diff --git a/static/style.css b/static/style.css index 944b0c5..adfb0bd 100644 --- a/static/style.css +++ b/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 */ /* diff --git a/uploads/testdata/.placeholder b/uploads/additional_file/.gitkeep similarity index 100% rename from uploads/testdata/.placeholder rename to uploads/additional_file/.gitkeep diff --git a/uploads/answer/.gitkeep b/uploads/answer/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/uploads/testdata/.gitkeep b/uploads/testdata/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/utility.js b/utility.js index 94e6839..c72a8ec 100644 --- a/utility.js +++ b/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; + } } }; diff --git a/views/article.ejs b/views/article.ejs index af68cda..3083dc7 100644 --- a/views/article.ejs +++ b/views/article.ejs @@ -1,17 +1,36 @@ <% this.title = article.title + ' - 帖子'; %> <% include header %>

<%= article.title %>

<%= article.user.username %><% if (article.user.nameplate) { %><%- article.user.nameplate %><% } %> 于 <%= syzoj.utils.formatDate(article.public_time) %> 发表,<%= syzoj.utils.formatDate(article.update_time) %> 最后更新 <% if (article.allowedEdit) { %> - 删除文章 + 删除文章 编辑文章 +

<% } %>

@@ -32,7 +51,26 @@
<%- comment.content %>
<% if (comment.allowedEdit) { %> - + + <% } %>
diff --git a/views/article_edit.ejs b/views/article_edit.ejs index 88b7e82..d696d24 100644 --- a/views/article_edit.ejs +++ b/views/article_edit.ejs @@ -17,6 +17,13 @@ + <% if (user && user.is_admin) { %> +
+ checked=""<% } %> name="is_notice" type="checkbox"> + +

选择后将显示在首页公告栏。

+
+ <% } %>
diff --git a/views/contest_edit.ejs b/views/contest_edit.ejs index acb91b4..e1cd562 100644 --- a/views/contest_edit.ejs +++ b/views/contest_edit.ejs @@ -22,19 +22,19 @@
- checked="checked"<% } %>> + disabled <% } %>type="radio" name="type" id="type-noi" value="noi"<% if (contest.type === 'noi') { %> checked="checked"<% } %>>
- checked="checked"<% } %>> + disabled <% } %>type="radio" name="type" id="type-ioi" value="ioi"<% if (contest.type === 'ioi') { %> checked="checked"<% } %>>
- checked="checked"<% } %>> + disabled <% } %>type="radio" name="type" id="type-acm" value="acm"<% if (contest.type === 'acm') { %> checked="checked"<% } %>>
@@ -51,6 +51,13 @@
+
+ +
+ checked<% } %> name="is_public"> + +
+
- + + +<% } else { %> + +<% } %> <% include footer %> diff --git a/views/problem_data.ejs b/views/problem_data.ejs index 81c1a31..5ac4d87 100644 --- a/views/problem_data.ejs +++ b/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 %>
-
-
-
- <% if (problem.testdata) { %> - <% - try { - let list = syzoj.utils.parseTestData(problem.testdata.getPath()); - %> - <% if (list.spj) { %> -

评测方式:Special Judge

- <% } else { %> -

评测方式:文本比较

- <% } %> - - - <% let i = 0; %> - <% for (let subtask of list) { %> - <% if (list.length !== 1) { %> - - - <% } else { %> - - - <% } %> - <% for (let testcase of subtask.cases) { %> - - - - - <% } %> - <% } %> - -

子任务 <%= ++i %>

<%= subtaskType[subtask.type] %>,总分值 <%= subtask.score %> -

单个子任务

<%= subtaskType[subtask.type] %> -
<%= testcase.input %><%= testcase.output %>
- <% } catch (e) { %> -

数据包错误:<%= e %>

+
+
+

测试点信息

+ <% include problem_testcases %> +
+
+

文件列表

+ <% if (testdata) { %> + + + + + + + <% if (problem.allowedEdit) { %> + + <% } %> + + + + <% if (testdata.zip) { %> + + + + + <% if (problem.allowedEdit) { %> + + <% } %> + <% } %> - <% } else { %> -

数据未上传

- <% } %> - -
-
-
-
- - -
-
- - -
-
- <% if (!problem.file_io) { %> + <% let i = 0; %> + <% if (testdata.files) for (let file of testdata.files) { %> + <% i++; %> +
+ + + + <% if (problem.allowedEdit) { %> + + <% } %> + + <% } %> + +
文件名文件大小下载删除
完整数据包<%- !testdata.zip.size ? '' : syzoj.utils.formatSize(testdata.zip.size) %>
<%= file.filename %><%= syzoj.utils.formatSize(file.size) %> + + + + + + + + +
+ <% } else { %> +

无测试数据

+ <% } %> + <% if (problem.allowedEdit) { %> +
- -
-
- - -
-
-
-
- - +
+ + +
+
提交
+ 返回题目
-
-
- - -
-
- - -
+ + -
- -<% include footer %> +
+<% include footer %> \ No newline at end of file diff --git a/views/problem_manage.ejs b/views/problem_manage.ejs new file mode 100644 index 0000000..23d419e --- /dev/null +++ b/views/problem_manage.ejs @@ -0,0 +1,144 @@ +<% this.title = '管理题目'; %> +<% include header %> +
+
+
+
+ <% include problem_testcases %> +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ <% if (!problem.file_io) { %> +
+ +
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+ + +
+
+ <% } else { %> +
+ +
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+ + +
+
+ <% } %> +
+
+ 为了避免系统出错,已有提交的题目不允许在提交答案和非提交答案之间更改。
+ 提交答案题目不需要设置时间限制、空间限制以及 IO 方式。
+ 提交时使用的答案文件名需要与测试数据中每个测试点输出文件相同,且后缀名为 .out。 +
+
+ + +
+
+ + +
+ + 返回题目 +
+
+
+
+ +<% include footer %> diff --git a/views/problem_testcases.ejs b/views/problem_testcases.ejs new file mode 100644 index 0000000..fddf6fd --- /dev/null +++ b/views/problem_testcases.ejs @@ -0,0 +1,42 @@ +<% +let subtaskType = { + sum: '测试点分数按百分比相加', + min: '取各测试点最低分', + mul: '测试点分数按百分比相乘' +}; +%> +<% if (testcases && testcases.error) { %> +

数据包错误:<%= testcases.error %>

+<% +} else if (testcases) { +%> + <% if (testcases.spj) { %> +

评测方式:Special Judge

+ <% } else { %> +

评测方式:文本比较

+ <% } %> + + + <% let i = 0; %> + <% for (let subtask of testcases) { %> + <% if (testcases.length !== 1) { %> + + + <% } else { %> + + + <% } %> + <% for (let testcase of subtask.cases) { %> + + + + + <% } %> + <% } %> + +

子任务 <%= ++i %>

<%= subtaskType[subtask.type] %>,总分值 <%= subtask.score %> +

单个子任务

<%= subtaskType[subtask.type] %> +
<%= testcase.input %><%= testcase.output %>
+<% } else { %> +

无测试数据

+<% } %> \ No newline at end of file diff --git a/views/sign_up.ejs b/views/sign_up.ejs index e0b05d9..3ce098d 100644 --- a/views/sign_up.ejs +++ b/views/sign_up.ejs @@ -14,10 +14,6 @@
-
@@ -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; diff --git a/views/statistics.ejs b/views/statistics.ejs index b7f2550..2f8a135 100644 --- a/views/statistics.ejs +++ b/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) {