diff --git a/config-example.json b/config-example.json index c37fe26..8b48891 100644 --- a/config-example.json +++ b/config-example.json @@ -25,7 +25,9 @@ "judge_state": 10, "ranklist": 20, "discussion": 10, - "article_comment": 10 + "article_comment": 10, + "contest": 10, + "edit_contest_problem_list": 10 }, "languages": [ "C++", diff --git a/models/article-comment.js b/models/article-comment.js index 8d6e966..d996eeb 100644 --- a/models/article-comment.js +++ b/models/article-comment.js @@ -33,7 +33,7 @@ let model = db.define('comment', { article_id: { type: Sequelize.INTEGER, references: { - model: Article.model, + model: 'article', key: 'id' } }, @@ -41,7 +41,7 @@ let model = db.define('comment', { user_id: { type: Sequelize.INTEGER, references: { - model: User.model, + model: 'user', key: 'id' } }, diff --git a/models/article.js b/models/article.js index a06d726..d3f9df1 100644 --- a/models/article.js +++ b/models/article.js @@ -33,7 +33,7 @@ let model = db.define('article', { user_id: { type: Sequelize.INTEGER, references: { - model: User.model, + model: 'user', key: 'id' } }, diff --git a/models/common.js b/models/common.js index 65c72b5..db7349e 100644 --- a/models/common.js +++ b/models/common.js @@ -87,13 +87,19 @@ class Model { return this.model.count({ where: where }); } - static async query(page, perpage, where, order) { - let records = await this.model.findAll({ - offset: (page - 1) * perpage, - limit: perpage, + static async query(paginate, where, order) { + let options = { where: where, order: order - }); + }; + if (Array.isArray(paginate)) { + options.offset = paginate[0] - 1; + options.limit = paginate[1] - paginate[0] + 1; + } else if (paginate) { + options.offset = (paginate.currPage - 1) * paginate.perPage; + options.limit = paginate.perPage; + } + let records = await this.model.findAll(options); return records.mapAsync(record => (this.fromRecord(record))); } } diff --git a/models/contest.js b/models/contest.js new file mode 100644 index 0000000..f9b96a7 --- /dev/null +++ b/models/contest.js @@ -0,0 +1,140 @@ +/* + * 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 User = syzoj.model('user'); +let Problem = syzoj.model('problem'); +let ContestRanklist = syzoj.model('contest_ranklist'); +let ContestPlayer = syzoj.model('contest_player'); + +let model = db.define('contest', { + id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, + title: { type: Sequelize.STRING(80) }, + start_time: { type: Sequelize.INTEGER }, + end_time: { type: Sequelize.INTEGER }, + + holder_id: { + type: Sequelize.INTEGER, + references: { + model: 'user', + key: 'id' + } + }, + + information: { type: Sequelize.TEXT }, + problems: { type: Sequelize.TEXT }, + + ranklist_id: { + type: Sequelize.INTEGER, + references: { + model: 'contest_ranklist', + key: 'id' + } + } +}, { + timestamps: false, + tableName: 'contest', + indexes: [ + { + fields: ['holder_id'], + }, + { + fields: ['ranklist_id'], + } + ] +}); + +let Model = require('./common'); +class Contest extends Model { + static async create(val) { + return Contest.fromRecord(Contest.model.build(Object.assign({ + title: '', + start_time: 0, + end_time: 0, + holder: 0, + ranklist_id: 0 + }, val))); + } + + async loadRelationships() { + this.holder = await User.fromID(this.holder_id); + this.ranklist = await ContestRanklist.fromID(this.ranklist_id); + } + + async isAllowedEditBy(user) { + return user && (user.is_admin || this.holder_id === user.id); + } + + async isAllowedSeeResultBy(user) { + return (user && (user.is_admin || this.holder_id === user.id)) || !(await this.isRunning()); + } + + async getProblems() { + return this.problems.split('|').map(x => parseInt(x)); + } + + async setProblems(s) { + let a = []; + await s.split('|').forEachAsync(async x => { + let problem = await Problem.fromID(x); + if (!problem) return; + a.push(x); + }); + this.problems = a.join('|'); + } + + async newSubmission(judge_state) { + let problems = await this.getProblems(); + if (!problems.includes(judge_state.problem_id)) throw 'No such problem in contest.'; + + let player = await ContestPlayer.findInContest({ + contest_id: this.id, + user_id: judge_state.user_id + }); + + if (!player) { + player = await ContestPlayer.create({ + contest_id: this.id, + user_id: judge_state.user_id + }); + } + + await player.updateScore(judge_state); + await player.save(); + + await this.loadRelationships(); + await this.ranklist.updatePlayer(player); + await this.ranklist.save(); + } + + async isRunning(now) { + if (!now) now = syzoj.utils.getCurrentTime(); + return now >= this.start_time && now < this.end_time; + } + + getModel() { return model; } +} + +Contest.model = model; + +module.exports = Contest; diff --git a/models/contest_player.js b/models/contest_player.js new file mode 100644 index 0000000..586e5ba --- /dev/null +++ b/models/contest_player.js @@ -0,0 +1,99 @@ +/* + * 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 User = syzoj.model('user'); +let Problem = syzoj.model('problem'); +let Contest = syzoj.model('contest'); + +let model = db.define('contest_player', { + id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, + contest_id: { + type: Sequelize.INTEGER, + references: { + model: 'contest', + key: 'id' + } + }, + user_id: { + type: Sequelize.INTEGER, + references: { + model: 'user', + key: 'id' + } + }, + + score: { type: Sequelize.INTEGER }, + score_details: { type: Sequelize.TEXT, json: true }, + time_spent: { type: Sequelize.INTEGER } +}, { + timestamps: false, + tableName: 'contest_player', + indexes: [ + { + fields: ['contest_id'], + }, + { + fields: ['user_id'], + } + ] +}); + +let Model = require('./common'); +class ContestPlayer extends Model { + static async create(val) { + return ContestPlayer.fromRecord(ContestPlayer.model.build(Object.assign({ + contest_id: 0, + user_id: 0, + score: 0, + score_details: '{}', + time_spent: 0 + }, val))); + } + + static async findInContest(where) { + return ContestPlayer.findOne({ where: where }); + } + + async loadRelationships() { + this.user = await User.fromID(this.user_id); + this.contest = await Contest.fromID(this.contest_id); + } + + async updateScore(judge_state) { + this.score_details[judge_state.problem_id] = { + score: judge_state.score, + judge_id: judge_state.id + }; + this.score = 0; + for (let x in this.score_details) { + this.score += this.score_details[x].score; + } + } + + getModel() { return model; } +} + +ContestPlayer.model = model; + +module.exports = ContestPlayer; diff --git a/models/contest_ranklist.js b/models/contest_ranklist.js new file mode 100644 index 0000000..6cc047f --- /dev/null +++ b/models/contest_ranklist.js @@ -0,0 +1,77 @@ +/* + * 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 User = syzoj.model('user'); +let Problem = syzoj.model('problem'); +let ContestPlayer = syzoj.model('contest_player'); + +let model = db.define('contest_ranklist', { + id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, + ranklist: { type: Sequelize.TEXT, json: true } +}, { + timestamps: false, + tableName: 'contest_ranklist' +}); + +let Model = require('./common'); +class ContestRanklist extends Model { + static async create(val) { + return ContestRanklist.fromRecord(ContestRanklist.model.build(Object.assign({ + ranklist: '{}' + }, val))); + } + + async getPlayers() { + let a = []; + for (let i = 1; i <= this.ranklist.player_num; i++) { + a.push(await ContestPlayer.fromID(this.ranklist[i])); + } + return a; + } + + async updatePlayer(player) { + let players = await this.getPlayers(), newPlayer = true; + for (let x of players) { + if (x.user_id === player.user_id) { + newPlayer = false; + break; + } + } + + if (newPlayer) { + players.push(player); + } + + players.sort((a, b) => b.score - a.score); + + this.ranklist = { player_num: players.length }; + for (let i = 0; i < players.length; i++) this.ranklist[i + 1] = players[i].id; + } + + getModel() { return model; } +} + +ContestRanklist.model = model; + +module.exports = ContestRanklist; diff --git a/models/judge_state.js b/models/judge_state.js index aa24b5f..1cd2102 100644 --- a/models/judge_state.js +++ b/models/judge_state.js @@ -24,6 +24,7 @@ let db = syzoj.db; let User = syzoj.model('user'); let Problem = syzoj.model('problem'); +let Contest = syzoj.model('contest'); let model = db.define('judge_state', { id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, @@ -108,13 +109,28 @@ class JudgeState extends Model { if (user && (user.is_admin || user.id === this.problem.user_id)) return true; else if (this.type === 0) return true; else if (this.type === 1) { - // TODO: contest - return false; + let contest = await Contest.fromID(this.type_info); + if (await contest.isRunning()) { + return false; + } else { + return true; + } } else if (this.type === 2) return false; } async isAllowedSeeCodeBy(user) { - return this.isAllowedSeeResultBy(user); + await this.loadRelationships(); + + if (user && (user.is_admin || user.id === this.problem.user_id)) return true; + else if (this.type === 0) return true; + else if (this.type === 1) { + let contest = await Contest.fromID(this.type_info); + if (await contest.isRunning()) { + return user && this.user_id === user.id; + } else { + return true; + } + } else if (this.type === 2) return false; } async updateResult(result) { @@ -131,6 +147,9 @@ class JudgeState extends Model { if (this.status === 'Accepted') this.problem.ac_num++; await this.user.save(); await this.problem.save(); + } else if (this.type === 1) { + let contest = await Contest.fromID(this.type_info); + await contest.newSubmission(this); } } diff --git a/models/problem.js b/models/problem.js index 2aead31..7386ea9 100644 --- a/models/problem.js +++ b/models/problem.js @@ -32,7 +32,7 @@ let model = db.define('problem', { user_id: { type: Sequelize.INTEGER, references: { - model: User.model, + model: 'user', key: 'id' } }, @@ -49,7 +49,7 @@ let model = db.define('problem', { testdata_id: { type: Sequelize.INTEGER, references: { - model: TestData.model, + model: 'file', key: 'id' } }, diff --git a/models/waiting_judge.js b/models/waiting_judge.js index e7816bd..12f1049 100644 --- a/models/waiting_judge.js +++ b/models/waiting_judge.js @@ -29,7 +29,7 @@ let model = db.define('waiting_judge', { judge_id: { type: Sequelize.INTEGER, references: { - model: JudgeState.model, + model: 'judge_state', key: 'id' } } diff --git a/modules/api_v2.js b/modules/api_v2.js new file mode 100644 index 0000000..833265f --- /dev/null +++ b/modules/api_v2.js @@ -0,0 +1,52 @@ +/* + * 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 Problem = syzoj.model('problem'); + +app.get('/api/v2/search/problem/:keyword*?', async (req, res) => { + try { + let keyword = req.params.keyword || ''; + let problems = await Problem.query(null, { + title: { like: `%${req.params.keyword}%` } + }, [['id', 'asc']]); + + let result = []; + + let id = parseInt(keyword); + if (id) { + let problemById = await Problem.fromID(parseInt(keyword)); + if (problemById && await problemById.isAllowedUseBy(res.locals.user)) { + result.push(problemById); + } + } + await problems.forEachAsync(async problem => { + if (await problem.isAllowedUseBy(res.locals.user) && result.length < syzoj.config.page.edit_contest_problem_list && problem.id !== id) { + result.push(problem); + } + }); + + result = result.map(x => ({ name: `#${x.id}. ${x.title}`, value: x.id })); + res.send({ success: true, results: result }); + } catch (e) { + syzoj.log(e); + res.send({ success: false }); + } +}); diff --git a/modules/contest.js b/modules/contest.js new file mode 100644 index 0000000..92f817f --- /dev/null +++ b/modules/contest.js @@ -0,0 +1,226 @@ +/* + * 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 Contest = syzoj.model('contest'); +let ContestRanklist = syzoj.model('contest_ranklist'); +let ContestPlayer = syzoj.model('contest_player'); +let Problem = syzoj.model('problem'); +let JudgeState = syzoj.model('judge_state'); +let User = syzoj.model('user'); + +app.get('/contest', async (req, res) => { + try { + let paginate = syzoj.utils.paginate(Contest.count(), req.query.page, syzoj.config.page.contest); + let contests = await Contest.query(paginate); + + await contests.forEachAsync(async x => x.information = await syzoj.utils.markdown(x.information)); + + res.render('contest_list', { + contests: contests, + paginate: paginate + }) + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }); + } +}); + +app.get('/contest/:id/edit', async (req, res) => { + try { + if (!res.locals.user || !res.locals.user.is_admin) throw 'Permission denied.'; + + let contest_id = parseInt(req.params.id); + let contest = await Contest.fromID(contest_id); + if (!contest) { + contest = await Contest.create(); + } + + let problems = await contest.problems.split('|').mapAsync(async id => await Problem.fromID(id)); + + res.render('edit_contest', { + contest: contest, + problems: problems + }); + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }); + } +}); + +app.post('/contest/:id/edit', async (req, res) => { + try { + if (!res.locals.user || !res.locals.user.is_admin) throw 'Permission denied.'; + + let contest_id = parseInt(req.params.id); + let contest = await Contest.fromID(contest_id); + if (!contest) { + contest = await Contest.create(); + + contest.holder_id = res.locals.user.id; + + let ranklist = await ContestRanklist.create(); + await ranklist.save(); + contest.ranklist_id = ranklist.id; + } + + contest.title = req.body.title; + if (!Array.isArray(req.body.problems)) req.body.problems = [req.body.problems]; + contest.problems = req.body.problems.join('|'); + contest.information = req.body.information; + contest.start_time = syzoj.utils.parseTime(req.body.start_time); + contest.end_time = syzoj.utils.parseTime(req.body.end_time); + + await contest.save(); + + res.redirect(syzoj.utils.makeUrl(['contest', contest.id])); + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }); + } +}); + +app.get('/contest/:id', async (req, res) => { + try { + let contest_id = parseInt(req.params.id); + + let contest = await Contest.fromID(contest_id); + if (!contest) throw 'No such contest.'; + + contest.allowedEdit = await contest.isAllowedEditBy(res.locals.user); + contest.running = await contest.isRunning(); + contest.information = await syzoj.utils.markdown(contest.information); + + let problems_id = await contest.getProblems(); + let problems = await problems_id.mapAsync(async id => await Problem.fromID(id)); + + let player = null; + + if (res.locals.user) { + player = await ContestPlayer.findInContest({ + contest_id: contest.id, + user_id: res.locals.user.id + }); + } + + problems = problems.map(x => ({ problem: x, status: null, judge_id: null })); + if (player) { + for (let problem of problems) { + if (player.score_details[problem.problem.id]) { + if (await contest.isRunning()) { + problem.status = true; + } else { + let judge_state = await JudgeState.fromID(player.score_details[problem.problem.id].judge_id); + problem.status = judge_state.status; + } + problem.judge_id = player.score_details[problem.problem.id].judge_id; + } else { + if (contest.isRunning()) { + problem.status = false; + } + } + } + } + + res.render('contest', { + contest: contest, + problems: problems + }); + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }); + } +}); + +app.get('/contest/:id/ranklist', async (req, res) => { + try { + let contest_id = parseInt(req.params.id); + let contest = await Contest.fromID(contest_id); + if (!contest) throw 'No such contest.'; + + if (!await contest.isAllowedSeeResultBy(res.locals.user)) throw 'Permission denied'; + + await contest.loadRelationships(); + + let players_id = []; + for (let i = 1; i <= contest.ranklist.ranklist.player_num; i++) players_id.push(contest.ranklist.ranklist[i]); + + let ranklist = await players_id.mapAsync(async player_id => { + let player = await ContestPlayer.fromID(player_id); + + let user = await User.fromID(player.user_id); + + return { + user: user, + player: player + }; + }); + + let problems_id = await contest.getProblems(); + let problems = await problems_id.mapAsync(async id => await Problem.fromID(id)); + + res.render('contest_ranklist', { + contest: contest, + ranklist: ranklist, + problems: problems + }); + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }); + } +}); + +app.get('/contest/:id/:pid', async (req, res) => { + try { + let contest_id = parseInt(req.params.id); + let contest = await Contest.fromID(contest_id); + if (!await contest.isRunning()) throw 'The contest has ended.' + + let problems_id = await contest.getProblems(); + + let pid = parseInt(req.params.pid); + if (!pid || pid < 1 || pid > problems_id.length) throw 'No such problem.'; + + let problem_id = problems_id[pid - 1]; + let problem = await Problem.fromID(problem_id); + + await syzoj.utils.markdown(problem, [ 'description', 'input_format', 'output_format', 'example', 'limit_and_hint' ]); + + res.render('contest_problem', { + contest: contest, + problem: problem + }); + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }); + } +}); diff --git a/modules/discussion.js b/modules/discussion.js index 275d1c5..3d5ff23 100644 --- a/modules/discussion.js +++ b/modules/discussion.js @@ -25,21 +25,14 @@ let User = syzoj.model('user'); app.get('/discussion', async (req, res) => { try { - let page = parseInt(req.query.page); - if (!page || page < 1) page = 1; - - let count = await Article.count(); - let pageCnt = Math.ceil(count / syzoj.config.page.discussion); - if (page > pageCnt) page = pageCnt; - - let articles = await Article.query(page, syzoj.config.page.discussion, null, [['public_time', 'asc']]); + let paginate = syzoj.utils.paginate(await Article.count(), req.query.page, syzoj.config.page.discussion); + let articles = await Article.query(paginate, null, [['public_time', 'asc']]); for (let article of articles) await article.loadRelationships(); res.render('discussion', { articles: articles, - pageCnt: pageCnt, - page: page + paginate: paginate }); } catch (e) { syzoj.log(e); @@ -62,28 +55,20 @@ app.get('/article/:id', async (req, res) => { let where = { article_id: id }; - let page = parseInt(req.query.page); - if (!page || page < 1) page = 1; - let count = await ArticleComment.count(where); - let pageCnt = Math.ceil(count / syzoj.config.page.article_comment); - if (page > pageCnt) page = pageCnt; - - let comments = []; - if (count) { - comments = await ArticleComment.query(page, syzoj.config.page.article_comment, where, [['public_time', 'asc']]); - - for (let comment of comments) { - comment.content = await syzoj.utils.markdown(comment.content); - comment.allowedEdit = await comment.isAllowedEditBy(res.locals.user); - await comment.loadRelationships(); - } + 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']]); + + for (let comment of comments) { + comment.content = await syzoj.utils.markdown(comment.content); + comment.allowedEdit = await comment.isAllowedEditBy(res.locals.user); + await comment.loadRelationships(); } res.render('article', { article: article, comments: comments, - pageCnt: pageCnt, - page: page + paginate: paginate }); } catch (e) { syzoj.log(e); diff --git a/modules/index.js b/modules/index.js index f45a63a..ad53efc 100644 --- a/modules/index.js +++ b/modules/index.js @@ -25,7 +25,7 @@ let Divine = require('syzoj-divine'); app.get('/', async (req, res) => { try { - let ranklist = await User.query(1, 10, { is_show: true }, [['ac_num', 'desc']]); + 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 => { diff --git a/modules/judge.js b/modules/judge.js index ebaab6b..ac274df 100644 --- a/modules/judge.js +++ b/modules/judge.js @@ -24,28 +24,20 @@ let User = syzoj.model('user'); app.get('/judge_state', async (req, res) => { try { - let page = parseInt(req.query.page); - if (!page || page < 1) page = 1; - let user = await User.fromName(req.query.submitter || ''); let where = {}; if (user) where.user_id = user.id; if (req.query.problem_id) where.problem_id = parseInt(req.query.problem_id); - let count = await JudgeState.count(where); - - let pageCnt = Math.ceil(count / syzoj.config.page.judge_state); - if (page > pageCnt) page = pageCnt; - - let judge_state = await JudgeState.query(page, syzoj.config.page.judge_state, where, [['submit_time', 'desc']]); + let paginate = syzoj.utils.paginate(await JudgeState.count(where), req.query.page, syzoj.config.page.judge_state); + let judge_state = await JudgeState.query(paginate, where, [['submit_time', 'desc']]); await judge_state.forEachAsync(async obj => obj.hidden = !(await obj.isAllowedSeeResultBy(res.locals.user))); await judge_state.forEachAsync(async obj => obj.allowedSeeCode = await obj.isAllowedSeeCodeBy(res.locals.user)); res.render('judge_state', { judge_state: judge_state, - pageCnt: pageCnt, - page: page, + paginate: paginate, form: { submitter: req.query.submitter || '', problem_id: req.query.problem_id || '' @@ -64,10 +56,10 @@ app.get('/judge_detail/:id', async (req, res) => { let id = parseInt(req.params.id); let judge = await JudgeState.fromID(id); - if (!await judge.isAllowedSeeCodeBy(res.locals.user)) throw 'Permission denied'; - judge.code = await syzoj.utils.highlight(judge.code, judge.language); if (judge.result.compiler_output) judge.result.compiler_output = syzoj.utils.ansiToHTML(judge.result.compiler_output); + judge.allowedSeeResult = await judge.isAllowedSeeResultBy(res.locals.user); + judge.allowedSeeCode = await judge.isAllowedSeeCodeBy(res.locals.user); res.render('judge_detail', { judge: judge diff --git a/modules/problem.js b/modules/problem.js index 8dad1b7..00ed540 100644 --- a/modules/problem.js +++ b/modules/problem.js @@ -22,17 +22,12 @@ let Problem = syzoj.model('problem'); let JudgeState = syzoj.model('judge_state'); let WaitingJudge = syzoj.model('waiting_judge'); +let Contest = syzoj.model('contest'); app.get('/problem', async (req, res) => { try { - let page = parseInt(req.query.page); - if (!page || page < 1) page = 1; - - let count = await Problem.count(); - let pageCnt = Math.ceil(count / syzoj.config.page.problem); - if (page > pageCnt) page = pageCnt; - - let problems = await Problem.query(page, syzoj.config.page.problem); + let paginate = syzoj.utils.paginate(await Problem.count(), req.query.page, syzoj.config.page.problem); + let problems = await Problem.query(paginate); await problems.forEachAsync(async problem => { problem.allowedEdit = await problem.isAllowedEditBy(res.locals.user); @@ -41,8 +36,7 @@ app.get('/problem', async (req, res) => { res.render('problem_set', { problems: problems, - pageCnt: pageCnt, - page: page + paginate: paginate }); } catch (e) { syzoj.log(e); @@ -221,11 +215,25 @@ app.post('/submit/:id', async (req, res) => { code: req.body.code, language: req.body.language, user_id: res.locals.user.id, - problem_id: req.params.id, - type: problem.is_public ? 0 : 2 + problem_id: req.params.id }); - await judge_state.save(); + let contest_id = parseInt(req.query.contest_id); + if (contest_id) { + let contest = await Contest.fromID(contest_id); + if (!contest) throw 'No such contest.'; + let problems_id = await contest.getProblems(); + if (!problems_id.includes(id)) throw 'No such problem.'; + + judge_state.type = 1; + judge_state.type_info = contest_id; + + await judge_state.save(); + await judge_state.updateRelatedInfo(); + } else { + judge_state.type = problem.is_public ? 0 : 2; + await judge_state.save(); + } let waiting_judge = await WaitingJudge.create({ judge_id: judge_state.id diff --git a/modules/user.js b/modules/user.js index 18d91be..beed682 100644 --- a/modules/user.js +++ b/modules/user.js @@ -24,23 +24,13 @@ let User = syzoj.model('user'); // Ranklist app.get('/ranklist', async (req, res) => { try { - let page = parseInt(req.query.page); - if (!page || page < 1) page = 1; - - let count = await User.count({ - is_show: true - }); - - let pageCnt = Math.ceil(count / syzoj.config.page.ranklist); - if (page > pageCnt) page = pageCnt; - - let ranklist = await User.query(page, syzoj.config.page.ranklist, { is_show: true }, [['ac_num', 'desc']]); + let paginate = syzoj.utils.paginate(await User.count({ is_show: true }), req.query.page, syzoj.config.page.ranklist); + let ranklist = await User.query(paginate, { is_show: true }, [['ac_num', 'desc']]); await ranklist.forEachAsync(async x => x.renderInformation()); res.render('ranklist', { ranklist: ranklist, - pageCnt: pageCnt, - page: page + paginate: paginate }); } catch (e) { console.log(e); @@ -98,6 +88,7 @@ app.get('/user/:id', async (req, res) => { let user = await User.fromID(id); user.ac_problems = await user.getACProblems(); user.articles = await user.getArticles(); + user.allowedEdit = await user.isAllowedEditBy(res.locals.user); res.render('user', { show_user: user diff --git a/utility.js b/utility.js index 8b2c596..feb1789 100644 --- a/utility.js +++ b/utility.js @@ -78,10 +78,9 @@ module.exports = { resolve(replaceUI(s)); }); } else { - let res = obj, cnt = 0; + let res = obj, cnt = keys.length; for (let key of keys) { if (res[key].trim()) { - cnt++; renderer(res[key], (s) => { res[key] = replaceUI(s); if (!--cnt) resolve(res); @@ -96,6 +95,9 @@ module.exports = { m.locale('zh-cn'); return m.format(format || 'L H:mm:ss'); }, + parseTime(s) { + return parseInt(+new Date(s) / 1000); + }, getCurrentTime() { return parseInt(+new Date / 1000); }, @@ -151,5 +153,21 @@ module.exports = { let Convert = require('ansi-to-html'); let convert = new Convert({ escapeXML: true }); return convert.toHtml(s); + }, + paginate(count, currPage, perPage) { + currPage = parseInt(currPage); + if (!currPage || currPage < 1) currPage = 1; + + let pageCnt = Math.ceil(count / perPage); + if (currPage > pageCnt) currPage = pageCnt; + + return { + currPage: currPage, + perPage: perPage, + pageCnt: pageCnt + }; + }, + removeTitleTag(s) { + return s.replace(/「[\S\s]+?」/, ''); } }; diff --git a/views/contest.ejs b/views/contest.ejs new file mode 100644 index 0000000..5fa3f71 --- /dev/null +++ b/views/contest.ejs @@ -0,0 +1,69 @@ +<% this.title = contest.title + ' - 比赛' %> +<% include header %> +
+

<%= contest.title %>

+
+ <% if (contest.allowedEdit || !contest.running) { %> +
+
+
+ 排行榜 + <% if (contest.allowedEdit) { %> + 编辑比赛 + <% } %> +
+
+
+ <% } %> +
+
+

描述

+
+ <%- contest.information %> +
+
+
+
+
+ + + + + + + + + + <% + let i = 0; + for (let problem of problems) { + i++; + %> + + + + + + <% } %> + +
状态题目代码
+ <% if (problem.judge_id) { %> + + <% if (problem.status === true) { %> + + <% } else if (problem.status !== false) { %> + + <%= problem.status %> + + <% } %> + <% } %> + <%= syzoj.utils.removeTitleTag(problem.problem.title) %> + <% if (problem.judge_id) { %> + + <% } %> +
+
+
+
+
+<% include footer %> diff --git a/views/contest_list.ejs b/views/contest_list.ejs new file mode 100644 index 0000000..ff30a6b --- /dev/null +++ b/views/contest_list.ejs @@ -0,0 +1,48 @@ +<% this.title = '比赛' %> +<% include header %> +
+ <% if (user && user.is_admin) { %> +
+
+ +
+
+ <% } %> + + + + + + + + + + + <% + for (let contest of contests) { + let now = syzoj.utils.getCurrentTime(); + let tag = ''; + %> + + <% if (now < contest.start_time) { %> + <% tag = '
未开始
' %> + <% } else if (now >= contest.start_time && now < contest.end_time) { %> + <% tag = '
进行中
' %> + <% } else { %> + <% tag = '
已结束
' %> + <% } %> + + + + + + + <% } %> + +
比赛名称开始时间结束时间描述
<%= contest.title %> <%- tag %><%= syzoj.utils.formatDate(contest.start_time) %><%= syzoj.utils.formatDate(contest.end_time) %><%- contest.information %>
+
+ <% include page %> +
+<% include footer %> diff --git a/views/contest_problem.ejs b/views/contest_problem.ejs new file mode 100644 index 0000000..e04f829 --- /dev/null +++ b/views/contest_problem.ejs @@ -0,0 +1,81 @@ +<% this.title = syzoj.utils.removeTitleTag(problem.title) + ' - ' + contest.title + ' - 比赛' %> +<% include header %> +
+
+

<%= syzoj.utils.removeTitleTag(problem.title) %>

+
+
+ 内存限制: <%= problem.memory_limit %> MiB + 时间限制: <%= problem.time_limit %> ms +
+
+ <% if (problem.file_io) { %> + 输入文件: <%= problem.file_io_input_name %> + 输出文件: <%= problem.file_io_output_name %> + <% } else { %> + 标准输入输出 + <% } %> +
+
+
+
+
+ +
+
+
+
+

题目描述

+
<%- problem.description %>
+
+
+
+
+

输入格式

+
<%- problem.input_format %>
+
+
+
+
+

输出格式

+
<%- problem.output_format %>
+
+
+
+
+

测试样例

+
<%- problem.example %>
+
+
+
+
+

数据范围与提示

+
<%- problem.limit_and_hint %>
+
+
+
+ +

提交代码

+
+
+ + +
+
+ +
+ +
+ +<% include footer %> diff --git a/views/contest_ranklist.ejs b/views/contest_ranklist.ejs new file mode 100644 index 0000000..d88d804 --- /dev/null +++ b/views/contest_ranklist.ejs @@ -0,0 +1,63 @@ +<%= this.title = '排名 - ' + contest.title %> +<% include header %> +
+ + + + + + <% for (let problem of problems) { %> + + <% } %> + + + + + <% + let i = 0; + for (let item of ranklist) { + i++ + %> + + + + <% for (let problem of problems) { %> + <% if (item.player.score_details[problem.id]) { %> + + <% } else { %> + + <% } %> + <% } %> + + + <% } %> + +
#用户名 + <%= syzoj.utils.removeTitleTag(problem.title) %> + 总分
+ <% if (i == 1) { %> +
+ <% } else if (i == 2) { %> +
+ <% } else if (i == 3) { %> +
+ <% } else { %> +
+ <% } %> + <%= i %> +
+
<%= item.user.username %> + <% if (item.user.nameplate) { %> + <%= item.user.nameplate %> + <% } %> + + + <%= item.player.score_details[problem.id].score %> + + + + <%= item.player.score %> + +
+
+<% include footer %> diff --git a/views/edit_contest.ejs b/views/edit_contest.ejs new file mode 100644 index 0000000..164af37 --- /dev/null +++ b/views/edit_contest.ejs @@ -0,0 +1,50 @@ +<% this.title = contest.id ? '编辑比赛' : '新建比赛' %> +<% include header %> +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +<% include footer %> diff --git a/views/header.ejs b/views/header.ejs index 19553a5..a51b4d0 100644 --- a/views/header.ejs +++ b/views/header.ejs @@ -17,6 +17,7 @@ <%= syzoj.config.title %> 首页 题库 + 比赛 评测 排名 讨论 diff --git a/views/judge_detail.ejs b/views/judge_detail.ejs index f4aa84e..df7c975 100644 --- a/views/judge_detail.ejs +++ b/views/judge_detail.ejs @@ -17,19 +17,27 @@ #<%= judge.id %> #<%= judge.problem_id %>. <%= judge.problem.title %> - <%= judge.result.status %> - <%= judge.result.score %> + <% if (judge.allowedSeeResult) { %> + <%= judge.result.status %> + <%= judge.result.score %> <%= judge.result.total_time %> + <% } else { %> + 隐藏 + 隐藏 + 隐藏 + <% } %> <%= judge.user.username %> <%= syzoj.utils.formatDate(judge.submit_time) %> + <% if (judge.allowedSeeCode) { %>
<%- judge.code %>
-<% if (judge.result.compiler_output && judge.result.status === 'Compile Error') { %> + <% } %> +<% if (judge.result.compiler_output && judge.result.status === 'Compile Error' && judge.allowedSeeCode) { %>

编译信息

<%- judge.result.compiler_output %>
-<% } else { %> +<% } else if (judge.allowedSeeResult) { %>
<% for (let i = 0; i < judge.result.case_num; i++) { %> <% let testcase = judge.result[i]; %> @@ -56,7 +64,7 @@ <% } %>
<% } %> - +