diff --git a/app.js b/app.js index 4f2217e..41cf90c 100644 --- a/app.js +++ b/app.js @@ -73,6 +73,7 @@ global.syzoj = { logging: syzoj.production ? false : syzoj.log }); global.Promise = Sequelize.Promise; + this.db.countQuery = async (sql, options) => (await this.db.query(`SELECT COUNT(*) FROM (${sql})`, options))[0][0]['COUNT(*)']; this.db.sync(); }, loadModules() { diff --git a/config-example.json b/config-example.json index 40ed4ac..b438802 100644 --- a/config-example.json +++ b/config-example.json @@ -22,6 +22,7 @@ }, "page": { "problem": 50, + "problem_statistics": 10, "judge_state": 10, "ranklist": 20, "discussion": 10, diff --git a/models/judge_state.js b/models/judge_state.js index 3735f20..60fdc29 100644 --- a/models/judge_state.js +++ b/models/judge_state.js @@ -33,6 +33,10 @@ let model = db.define('judge_state', { status: { type: Sequelize.STRING(50) }, score: { type: Sequelize.INTEGER }, + total_time: { type: Sequelize.INTEGER }, + pending: { type: Sequelize.BOOLEAN }, + max_memory: { type: Sequelize.INTEGER }, + result: { type: Sequelize.TEXT('medium'), json: true }, user_id: { @@ -92,9 +96,13 @@ class JudgeState extends Model { type: 0, type_info: '', + pending: true, + score: 0, + total_time: 0, + max_memory: 0, status: 'Waiting', - result: '{ "status": "Waiting", "total_time": 0, "total_memory": 0, "score": 0, "case_num": 0, "compiler_output": "" }' + result: '{ "status": "Waiting", "total_time": 0, "max_memory": 0, "score": 0, "case_num": 0, "compiler_output": "", "pending": true }' }, val))); } @@ -135,7 +143,10 @@ class JudgeState extends Model { async updateResult(result) { this.score = result.score; + this.pending = result.pending; this.status = result.status; + this.total_time = result.total_time; + this.max_memory = result.max_memory; this.result = result; } diff --git a/models/problem.js b/models/problem.js index ffe77e7..4be6e00 100644 --- a/models/problem.js +++ b/models/problem.js @@ -19,6 +19,124 @@ 'use strict'; +let statisticsStatements = { + fastest: +'\ +SELECT \ + DISTINCT(`user_id`) AS `user_id`, \ + ( \ + SELECT \ + `id` \ + FROM `judge_state` `inner_table` \ + WHERE `problem_id` = `outer_table`.`problem_id` AND `user_id` = `outer_table`.`user_id` AND `status` = "Accepted" AND `type` = 0 \ + ORDER BY `total_time` ASC \ + ) AS `id`, \ + ( \ + SELECT \ + `total_time` \ + FROM `judge_state` `inner_table` \ + WHERE `problem_id` = `outer_table`.`problem_id` AND `user_id` = `outer_table`.`user_id` AND `status` = "Accepted" AND `type` = 0 \ + ORDER BY `total_time` ASC \ + ) AS `total_time` \ +FROM `judge_state` `outer_table` \ +WHERE \ + `problem_id` = __PROBLEM_ID__ AND `status` = "Accepted" AND `type` = 0 \ +ORDER BY `total_time` ASC \ +', + slowest: +' \ +SELECT \ + DISTINCT(`user_id`) AS `user_id`, \ + ( \ + SELECT \ + `id` \ + FROM `judge_state` `inner_table` \ + WHERE `problem_id` = `outer_table`.`problem_id` AND `user_id` = `outer_table`.`user_id` AND `status` = "Accepted" AND `type` = 0 \ + ORDER BY `total_time` DESC \ + ) AS `id`, \ + ( \ + SELECT \ + `total_time` \ + FROM `judge_state` `inner_table` \ + WHERE `problem_id` = `outer_table`.`problem_id` AND `user_id` = `outer_table`.`user_id` AND `status` = "Accepted" AND `type` = 0 \ + ORDER BY `total_time` DESC \ + ) AS `total_time` \ +FROM `judge_state` `outer_table` \ +WHERE \ + `problem_id` = __PROBLEM_ID__ AND `status` = "Accepted" AND `type` = 0 \ +ORDER BY `total_time` DESC \ +', + shortest: +' \ +SELECT \ + DISTINCT(`user_id`) AS `user_id`, \ + ( \ + SELECT \ + `id` \ + FROM `judge_state` `inner_table` \ + WHERE `problem_id` = `outer_table`.`problem_id` AND `user_id` = `outer_table`.`user_id` AND `status` = "Accepted" AND `type` = 0 \ + ORDER BY LENGTH(`code`) ASC \ + ) AS `id`, \ + ( \ + SELECT \ + LENGTH(`code`) \ + 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 LENGTH(`code`) ASC \ + ) AS `code_length` \ +FROM `judge_state` `outer_table` \ +WHERE \ + `problem_id` = __PROBLEM_ID__ AND `status` = "Accepted" AND `type` = 0 \ +ORDER BY `code_length` ASC \ +', + longest: +' \ +SELECT \ + DISTINCT(`user_id`) AS `user_id`, \ + ( \ + SELECT \ + `id` \ + FROM `judge_state` `inner_table` \ + WHERE `problem_id` = `outer_table`.`problem_id` AND `user_id` = `outer_table`.`user_id` AND `status` = "Accepted" AND `type` = 0 \ + ORDER BY LENGTH(`code`) DESC \ + ) AS `id`, \ + ( \ + SELECT \ + LENGTH(`code`) \ + 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 LENGTH(`code`) DESC \ + ) AS `code_length` \ +FROM `judge_state` `outer_table` \ +WHERE \ + `problem_id` = __PROBLEM_ID__ AND `status` = "Accepted" AND `type` = 0 \ +ORDER BY `code_length` DESC \ +', + earliest: +' \ +SELECT \ + DISTINCT(`user_id`) AS `user_id`, \ + ( \ + SELECT \ + `id` \ + FROM `judge_state` `inner_table` \ + WHERE `problem_id` = `outer_table`.`problem_id` AND `user_id` = `outer_table`.`user_id` AND `status` = "Accepted" AND `type` = 0 \ + ORDER BY `submit_time` ASC \ + ) AS `id`, \ + ( \ + SELECT \ + `submit_time` \ + FROM `judge_state` `inner_table` \ + WHERE `problem_id` = `outer_table`.`problem_id` AND `user_id` = `outer_table`.`user_id` AND `status` = "Accepted" AND `type` = 0 \ + ORDER BY `submit_time` ASC \ + ) AS `submit_time` \ +FROM `judge_state` `outer_table` \ +WHERE \ + `problem_id` = __PROBLEM_ID__ AND `status` = "Accepted" AND `type` = 0 \ +ORDER BY `submit_time` ASC \ +' +}; + let Sequelize = require('sequelize'); let db = syzoj.db; @@ -157,6 +275,63 @@ class Problem extends Model { }); } + // type: fastest / slowest / shortest / longest / earliest + async countStatistics(type) { + let statement = statisticsStatements[type]; + if (!statement) return null; + + statement = statement.replace('__PROBLEM_ID__', this.id); + return await db.countQuery(statement); + } + + // type: fastest / slowest / shortest / longest / earliest + async getStatistics(type, paginate) { + let statistics = { + type: type, + judge_state: null, + scoreDistribution: null, + prefixSum: null, + suffixSum: null + }; + + let statement = statisticsStatements[type]; + if (!statement) return null; + + statement = statement.replace('__PROBLEM_ID__', this.id); + let a = (await db.query(statement + `LIMIT ${paginate.perPage} OFFSET ${(paginate.currPage - 1) * paginate.perPage}`))[0]; + + let JudgeState = syzoj.model('judge_state'); + statistics.judge_state = await a.mapAsync(async x => JudgeState.fromID(x.id)); + + a = (await db.query('SELECT `score`, COUNT(*) AS `count` FROM `judge_state` WHERE `problem_id` = __PROBLEM_ID__ AND `type` = 0 AND `pending` = 0 GROUP BY `score`'.replace('__PROBLEM_ID__', this.id)))[0]; + + let scoreCount = []; + for (let score of a) { + score.score = Math.min(Math.round(score.score), 100); + scoreCount[score.score] = score.count; + } + if (scoreCount[0] === undefined) scoreCount[0] = 0; + if (scoreCount[100] === undefined) scoreCount[100] = 0; + + statistics.scoreDistribution = []; + for (let i = 0; i < scoreCount.length; i++) { + if (scoreCount[i] !== undefined) statistics.scoreDistribution.push({ score: i, count: scoreCount[i] }); + } + + statistics.prefixSum = JSON.parse(JSON.stringify(statistics.scoreDistribution)); + statistics.suffixSum = JSON.parse(JSON.stringify(statistics.scoreDistribution)); + + for (let i = 1; i < statistics.prefixSum.length; i++) { + statistics.prefixSum[i].count += statistics.prefixSum[i - 1].count; + } + + for (let i = statistics.prefixSum.length - 1; i >= 1; i--) { + statistics.suffixSum[i - 1].count += statistics.suffixSum[i].count; + } + + return statistics; + } + getModel() { return model; } } diff --git a/modules/api.js b/modules/api.js index 77bca5f..dbd45b5 100644 --- a/modules/api.js +++ b/modules/api.js @@ -76,7 +76,6 @@ app.post('/api/sign_up', async (req, res) => { app.post('/api/markdown', async (req, res) => { try { let s = await syzoj.utils.markdown(req.body.s.toString()); - console.log(s); res.send(s); } catch (e) { syzoj.log(e); diff --git a/modules/problem.js b/modules/problem.js index 4093f31..a48636e 100644 --- a/modules/problem.js +++ b/modules/problem.js @@ -256,3 +256,32 @@ app.get('/problem/:id/download', async (req, res) => { }); } }); + +app.get('/problem/:id/statistics/:type', async (req, res) => { + try { + let id = parseInt(req.params.id); + let problem = await Problem.fromID(id); + + if (!problem) throw 'No such problem'; + if (!await problem.isAllowedUseBy(res.locals.user)) throw 'Permission denied'; + + let count = await problem.countStatistics(req.params.type); + if (count === null) throw 'No such type'; + + let paginate = syzoj.utils.paginate(count, req.query.page, syzoj.config.page.problem_statistics); + let statistics = await problem.getStatistics(req.params.type, paginate); + + await statistics.judge_state.forEachAsync(async x => x.loadRelationships()); + + res.render('statistics', { + statistics: statistics, + paginate: paginate, + problem: problem + }); + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }); + } +}); diff --git a/modules/user.js b/modules/user.js index 6066240..08cce43 100644 --- a/modules/user.js +++ b/modules/user.js @@ -33,7 +33,7 @@ app.get('/ranklist', async (req, res) => { paginate: paginate }); } catch (e) { - console.log(e); + syzoj.log(e); res.render('error', { err: e }); @@ -46,7 +46,7 @@ app.get('/find_user', async (req, res) => { if (!user) throw `Can't find user ${req.query.nickname}`; res.redirect(syzoj.utils.makeUrl(['user', user.id])); } catch (e) { - console.log(e); + syzoj.log(e); res.render('error', { err: e }); @@ -98,7 +98,7 @@ app.get('/user/:id', async (req, res) => { statistics: statistics }); } catch (e) { - console.log(e); + syzoj.log(e); res.render('error', { err: e }); @@ -121,7 +121,7 @@ app.get('/user/:id/edit', async (req, res) => { error_info: null }); } catch (e) { - console.log(e); + syzoj.log(e); res.render('error', { err: e }); diff --git a/static/style.css b/static/style.css index abdf5f4..38d21c2 100644 --- a/static/style.css +++ b/static/style.css @@ -1,5 +1,5 @@ .main.container { - margin-top: 6em; + margin-top: 5.5em; } .padding { @@ -25,6 +25,10 @@ th { white-space: nowrap; } +pre { + tab-size: 4; +} + /* status color */ /* diff --git a/utility.js b/utility.js index ae5def6..19e2b1c 100644 --- a/utility.js +++ b/utility.js @@ -42,7 +42,6 @@ function escapeHTML(s) { } function highlightPygmentize(code, lang, cb) { - code = code.split('\t').join(' '); pygmentize({ lang: lang, format: 'html', diff --git a/views/edit_article.ejs b/views/edit_article.ejs index d13dbe6..39dcdc4 100644 --- a/views/edit_article.ejs +++ b/views/edit_article.ejs @@ -19,7 +19,7 @@ -
+
@@ -33,7 +33,7 @@ $(function () { function render(output, input) { $.post('/api/markdown', { s: input.val() }, function (s) { - console.log(s); + // console.log(s); output.html(s); }); } diff --git a/views/edit_problem.ejs b/views/edit_problem.ejs index 46087fa..3a8ccfd 100644 --- a/views/edit_problem.ejs +++ b/views/edit_problem.ejs @@ -53,7 +53,7 @@ $(function () { function render(output, input) { $.post('/api/markdown', { s: input.val() }, function (s) { - console.log(s); + // console.log(s); output.html(s); }); } diff --git a/views/header.ejs b/views/header.ejs index 466c4ce..5282ed9 100644 --- a/views/header.ejs +++ b/views/header.ejs @@ -8,6 +8,7 @@ + diff --git a/views/problem.ejs b/views/problem.ejs index fcc9dd7..ba05801 100644 --- a/views/problem.ejs +++ b/views/problem.ejs @@ -50,7 +50,12 @@ if (contest) { 返回比赛 <% } else { %> 提交记录 + 统计 下载测试数据 + <% } %> +
+ <% if (!contest) { %> +
<% if (problem.allowedEdit) { %> 编辑题面 上传测试数据 @@ -62,38 +67,38 @@ if (contest) { <% } %> <% } %> - <% } %> -
+ + <% } %>

题目描述

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

输入格式

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

输出格式

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

测试样例

-
<%- problem.example %>
+

样例

+
<%- problem.example %>

数据范围与提示

-
<%- problem.limit_and_hint %>
+
<%- problem.limit_and_hint %>
@@ -139,6 +144,7 @@ var editor = ace.edit("editor"); 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'; diff --git a/views/statistics.ejs b/views/statistics.ejs new file mode 100644 index 0000000..9d10840 --- /dev/null +++ b/views/statistics.ejs @@ -0,0 +1,175 @@ +<% +this.title = '统计'; +let types = { + fastest: '最快', + slowest: '最慢', + shortest: '最短', + longest: '最长', + earliest: '最早' +}; +%> +<% include header %> + + + +
+

+ 满分提交 + + + +

+ + + + + + + + + + + + + + + + <% for (let judge of statistics.judge_state) { %> + <% include util %> + + + + + + + + + + + + <% } %> + +
编号题目状态分数总时间内存代码提交者提交时间
#<%= judge.id %>#<%= judge.problem_id %>. <%= judge.problem.title %> + + + <%= judge.status %> + + <%= judge.result.score %><%= judge.result.total_time %> ms<%= parseInt(judge.result.max_memory) || 0 %> K<%= syzoj.config.languages[judge.language].show %> / <%= syzoj.utils.formatSize(judge.code.length) %><%= judge.user.username %><% if (judge.user.nameplate) { %><%- judge.user.nameplate %><% } %><%= syzoj.utils.formatDate(judge.submit_time) %>
+
+ <% include page %> +
+

+ 得分分布 +

+
+ +

+ 前缀和 +

+
+ +

+ 后缀和 +

+
+ +
+<% include footer %>