Browse Source

Add problem statistics

pull/6/head
Menci 8 years ago
parent
commit
3752bef5ec
  1. 1
      app.js
  2. 1
      config-example.json
  3. 13
      models/judge_state.js
  4. 175
      models/problem.js
  5. 1
      modules/api.js
  6. 29
      modules/problem.js
  7. 8
      modules/user.js
  8. 6
      static/style.css
  9. 1
      utility.js
  10. 2
      views/edit_article.ejs
  11. 2
      views/edit_problem.ejs
  12. 1
      views/header.ejs
  13. 20
      views/problem.ejs
  14. 175
      views/statistics.ejs

1
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() {

1
config-example.json

@ -22,6 +22,7 @@
},
"page": {
"problem": 50,
"problem_statistics": 10,
"judge_state": 10,
"ranklist": 20,
"discussion": 10,

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

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

1
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);

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

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

6
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 */
/*

1
utility.js

@ -42,7 +42,6 @@ function escapeHTML(s) {
}
function highlightPygmentize(code, lang, cb) {
code = code.split('\t').join(' ');
pygmentize({
lang: lang,
format: 'html',

2
views/edit_article.ejs

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

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

1
views/header.ejs

@ -8,6 +8,7 @@
<link href="//cdn.bootcss.com/semantic-ui/2.2.6/semantic.min.css" rel="stylesheet">
<link href="/tomorrow.css" rel="stylesheet">
<link href="//cdn.bootcss.com/KaTeX/0.6.0/katex.min.css" rel="stylesheet">
<link href="//cdn.bootcss.com/morris.js/0.5.1/morris.css" rel="stylesheet">
<link href="https://cdn.moefont.com/fonts/css?family=Raleway:300" rel="stylesheet">
<link href="/style.css" rel="stylesheet">
<script src="//cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>

20
views/problem.ejs

@ -50,7 +50,12 @@ if (contest) {
<a href="<%= syzoj.utils.makeUrl(['contest', contest.id]) %>" class="ui positive button">返回比赛</a>
<% } else { %>
<a class="small ui positive button" href="<%= syzoj.utils.makeUrl(['judge_state'], { problem_id: problem.id }) %>">提交记录</a>
<a class="small ui brown button" href="<%= syzoj.utils.makeUrl(['problem', problem.id, 'statistics', 'fastest']) %>">统计</a>
<a class="small ui yellow button" href="<%= syzoj.utils.makeUrl(['problem', problem.id, 'download']) %>">下载测试数据</a>
<% } %>
</div>
<% if (!contest) { %>
<div class="ui buttons right floated">
<% if (problem.allowedEdit) { %>
<a class="small ui button" href="<%= syzoj.utils.makeUrl(['problem', problem.id, 'edit']) %>">编辑题面</a>
<a class="small ui button" href="<%= syzoj.utils.makeUrl(['problem', problem.id, 'upload']) %>">上传测试数据</a>
@ -62,38 +67,38 @@ if (contest) {
<button class="small ui button" id="public">公开</button>
<% } %>
<% } %>
<% } %>
</div>
<% } %>
</div>
</div>
<div class="row">
<div class="column">
<h4 class="ui top attached block header">题目描述</h4>
<div class="ui bottom attached segment" id="description"><%- problem.description %></div>
<div class="ui bottom attached segment"><%- problem.description %></div>
</div>
</div>
<div class="row">
<div class="column">
<h4 class="ui top attached block header">输入格式</h4>
<div class="ui bottom attached segment" id="input"><%- problem.input_format %></div>
<div class="ui bottom attached segment"><%- problem.input_format %></div>
</div>
</div>
<div class="row">
<div class="column">
<h4 class="ui top attached block header">输出格式</h4>
<div class="ui bottom attached segment" id="output"><%- problem.output_format %></div>
<div class="ui bottom attached segment"><%- problem.output_format %></div>
</div>
</div>
<div class="row">
<div class="column">
<h4 class="ui top attached block header">测试样例</h4>
<div class="ui bottom attached segment" id="example"><%- problem.example %></div>
<h4 class="ui top attached block header">样例</h4>
<div class="ui bottom attached segment"><%- problem.example %></div>
</div>
</div>
<div class="row">
<div class="column">
<h4 class="ui top attached block header">数据范围与提示</h4>
<div class="ui bottom attached segment" id="hint"><%- problem.limit_and_hint %></div>
<div class="ui bottom attached segment"><%- problem.limit_and_hint %></div>
</div>
</div>
<div class="row">
@ -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';

175
views/statistics.ejs

@ -0,0 +1,175 @@
<%
this.title = '统计';
let types = {
fastest: '最快',
slowest: '最慢',
shortest: '最短',
longest: '最长',
earliest: '最早'
};
%>
<% include header %>
<script src="//cdn.bootcss.com/raphael/2.2.7/raphael.min.js"></script>
<script src="//cdn.bootcss.com/morris.js/0.5.1/morris.min.js"></script>
<script>
function getColorOfScore(score) {
let color = [];
color[0] = 'red';
color[1] = '#ff4b00';
color[2] = '#ff6200';
color[3] = '#ffa900';
color[4] = '#ffd800';
color[5] = '#c8ff00';
color[6] = '#a5ff00';
color[7] = '#52ff00';
color[8] = '#41f741';
color[9] = '#34d034';
color[10] = 'forestgreen';
return color[parseInt(score / 10)];
}
</script>
<div class="padding">
<h1 style="text-align: center; margin-bottom: 30px; ">
满分提交
<span class="ui header" style="margin-left: 10px; ">
<div class="ui compact menu">
<div class="ui simple dropdown item">
<%= types[statistics.type] %>
<i class="dropdown icon"></i>
<div class="menu">
<% for (let type in types) { %>
<% if (type !== statistics.type) { %>
<a class="item" href="<%= syzoj.utils.makeUrl(['problem', problem.id, 'statistics', type]) %>"><%= types[type] %></a>
<% } %>
<% } %>
</div>
</div>
</div>
</span>
</h1>
<table class="ui very basic center aligned table" style="white-space: nowrap; ">
<thead>
<tr>
<th>编号</th>
<th>题目</th>
<th>状态</th>
<th>分数</th>
<th>总时间</th>
<th>内存</th>
<th>代码</th>
<th>提交者</th>
<th>提交时间</th>
</tr>
</thead>
<tbody>
<% for (let judge of statistics.judge_state) { %>
<% include util %>
<tr>
<td><a href="<%= syzoj.utils.makeUrl(['judge_detail', judge.id]) %>">#<%= judge.id %></a></td>
<td><a href="<%= syzoj.utils.makeUrl(['problem', judge.problem_id]) %>">#<%= judge.problem_id %>. <%= judge.problem.title %></a></td>
<td><a href="<%= syzoj.utils.makeUrl(['judge_detail', judge.id]) %>">
<span class="status <%= getStatusMeta(judge.status).toLowerCase().split(' ').join('_') %>">
<i class="<%= icon[getStatusMeta(judge.status)] || 'remove' %> icon"></i>
<%= judge.status %>
</span>
</a></td>
<td><a href="<%= syzoj.utils.makeUrl(['judge_detail', judge.id]) %>"><span class="score score_<%= parseInt(judge.result.score / 10) || 0 %>"><%= judge.result.score %></span></a></td>
<td><%= judge.result.total_time %> ms</td>
<td><%= parseInt(judge.result.max_memory) || 0 %> K</td>
<td><a href="<%= syzoj.utils.makeUrl(['judge_detail', judge.id]) %>"><%= syzoj.config.languages[judge.language].show %></a> / <%= syzoj.utils.formatSize(judge.code.length) %></td>
<td><a href="<%= syzoj.utils.makeUrl(['user', judge.user_id]) %>"><%= judge.user.username %></a><% if (judge.user.nameplate) { %><%- judge.user.nameplate %><% } %></td>
<td><%= syzoj.utils.formatDate(judge.submit_time) %></td>
</tr>
<% } %>
</tbody>
</table>
<br>
<% include page %>
<br>
<h1 style="text-align: center; ">
得分分布
</h1>
<div id="score-distribution-chart" style="height: 250px;"></div>
<script type="text/javascript">
new Morris.Bar({
element: 'score-distribution-chart',
data: <%- JSON.stringify(statistics.scoreDistribution) %>,
barColors: function(r, s, type) {
return getColorOfScore(r.label);
},
xkey: 'score',
ykeys: ['count'],
labels: ['number'],
hoverCallback: function(index, options, content, row) {
var scr = row.score;
return '<div class="morris-hover-row-label">分数:' + scr + '</div><div class="morris-hover-point">数量:' + row.count + '</div>';
},
resize: true
});
</script>
<h1 style="text-align: center; ">
前缀和
</h1>
<div id="score-distribution-chart-pre" style="height: 250px;"></div>
<script type="text/javascript">
<%
for (let i in statistics.prefixSum) {
statistics.prefixSum[i].score *= 100;
}
%>
new Morris.Line({
element: 'score-distribution-chart-pre',
data: <%- JSON.stringify(statistics.prefixSum) %>,
xkey: 'score',
ykeys: ['count'],
labels: ['number'],
lineColors: function(row, sidx, type) {
if (type == 'line') {
return '#0b62a4';
}
return getColorOfScore(row.src.score / 100);
},
xLabelFormat: function(x) {
return (x.getTime() / 100).toString();
},
hoverCallback: function(index, options, content, row) {
var scr = row.score / 100;
return '<div class="morris-hover-row-label">分数:≤ ' + scr + '</div><div class="morris-hover-point">数量:' + row.count + '</div>';
},
resize: true
});
</script>
<h1 style="text-align: center; ">
后缀和
</h1>
<div id="score-distribution-chart-suf" style="height: 250px;"></div>
<script type="text/javascript">
<%
for (let i in statistics.suffixSum) {
statistics.suffixSum[i].score *= 100;
}
%>
new Morris.Line({
element: 'score-distribution-chart-suf',
data: <%- JSON.stringify(statistics.suffixSum) %>,
xkey: 'score',
ykeys: ['count'],
labels: ['number'],
lineColors: function(row, sidx, type) {
if (type == 'line') {
return '#0b62a4';
}
return getColorOfScore(row.src.score / 100);
},
xLabelFormat: function(x) {
return (x.getTime() / 100).toString();
},
hoverCallback: function(index, options, content, row) {
var scr = row.score / 100;
return '<div class="morris-hover-row-label">分数:≥ ' + scr + '</div><div class="morris-hover-point">数量:' + row.count + '</div>';
},
resize: true
});
</script>
</div>
<% include footer %>
Loading…
Cancel
Save