Browse Source

Add problem tag support

master
Menci 8 years ago
parent
commit
5b376ce5c9
  1. 18
      app.js
  2. 3
      config-example.json
  3. 43
      models/common.js
  4. 18
      models/problem.js
  5. 54
      models/problem_tag.js
  6. 62
      models/problem_tag_map.js
  7. 20
      modules/api_v2.js
  8. 83
      modules/problem.js
  9. 78
      modules/problem_tag.js
  10. 5
      utility.js
  11. 4
      views/contest_edit.ejs
  12. 25
      views/problem.ejs
  13. 41
      views/problem_edit.ejs
  14. 95
      views/problems.ejs

18
app.js

@ -74,7 +74,8 @@ global.syzoj = {
});
global.Promise = Sequelize.Promise;
this.db.countQuery = async (sql, options) => (await this.db.query(`SELECT COUNT(*) FROM (${sql}) AS \`__tmp_table\``, options))[0][0]['COUNT(*)'];
this.db.sync();
this.loadModels();
},
loadModules() {
fs.readdir('./modules/', (err, files) => {
@ -86,9 +87,20 @@ global.syzoj = {
.forEach((file) => this.modules.push(require(`./modules/${file}`)));
});
},
loadModels() {
fs.readdir('./models/', (err, files) => {
if (err) {
this.log(err);
return;
}
files.filter((file) => file.endsWith('.js'))
.forEach((file) => require(`./models/${file}`));
this.db.sync();
});
},
model(name) {
if (this.models[name] !== undefined) return this.models[name];
else return this.models[name] = require(`./models/${name}`);
return require(`./models/${name}`);
},
loadHooks() {
let Session = require('express-session');

3
config-example.json

@ -28,7 +28,8 @@
"discussion": 10,
"article_comment": 10,
"contest": 10,
"edit_contest_problem_list": 10
"edit_contest_problem_list": 10,
"edit_problem_tag_list": 10
},
"languages": {
"cpp": {

43
models/common.js

@ -84,24 +84,41 @@ class Model {
}
static async count(where) {
// count(sql)
if (typeof where === 'string') {
let sql = where;
return syzoj.db.countQuery(sql);
}
// count(where)
return this.model.count({ where: where });
}
static async query(paginate, where, order) {
if (paginate && !Array.isArray(paginate) && !paginate.pageCnt) return [];
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 = [];
if (typeof paginate === 'string') {
// query(sql)
let sql = paginate;
records = await syzoj.db.query(sql, { model: this.model });
} else {
if (paginate && !Array.isArray(paginate) && !paginate.pageCnt) return [];
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;
}
records = await this.model.findAll(options);
}
let records = await this.model.findAll(options);
return records.mapAsync(record => (this.fromRecord(record)));
}
}

18
models/problem.js

@ -353,6 +353,24 @@ class Problem extends Model {
return statistics;
}
async getTags() {
let ProblemTagMap = syzoj.model('problem_tag_map');
let maps = await ProblemTagMap.query(null, {
problem_id: this.id
});
let ProblemTag = syzoj.model('problem_tag');
let res = await maps.mapAsync(async map => {
return ProblemTag.fromID(map.tag_id);
});
res.sort((a, b) => {
return a.name > b.name ? 1 : -1;
});
return res;
}
getModel() { return model; }
}

54
models/problem_tag.js

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

62
models/problem_tag_map.js

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

20
modules/api_v2.js

@ -20,8 +20,9 @@
'use strict';
let Problem = syzoj.model('problem');
let ProblemTag = syzoj.model('problem_tag');
app.get('/api/v2/search/problem/:keyword*?', async (req, res) => {
app.get('/api/v2/search/problems/:keyword*?', async (req, res) => {
try {
let keyword = req.params.keyword || '';
let problems = await Problem.query(null, {
@ -51,6 +52,23 @@ app.get('/api/v2/search/problem/:keyword*?', async (req, res) => {
}
});
app.get('/api/v2/search/tags/:keyword*?', async (req, res) => {
try {
let keyword = req.params.keyword || '';
let tags = await ProblemTag.query(null, {
name: { like: `%${req.params.keyword}%` }
}, [['name', 'asc']]);
let result = tags.slice(0, syzoj.config.page.edit_problem_tag_list);
result = result.map(x => ({ name: x.name, value: x.id }));
res.send({ success: true, results: result });
} catch (e) {
syzoj.log(e);
res.send({ success: false });
}
});
app.get('/api/v2/hitokoto', async (req, res) => {
try {
res.send(await syzoj.utils.hitokoto());

83
modules/problem.js

@ -23,6 +23,8 @@ let Problem = syzoj.model('problem');
let JudgeState = syzoj.model('judge_state');
let WaitingJudge = syzoj.model('waiting_judge');
let Contest = syzoj.model('contest');
let ProblemTag = syzoj.model('problem_tag');
let ProblemTagMap = syzoj.model('problem_tag_map');
app.get('/problems', async (req, res) => {
try {
@ -32,6 +34,7 @@ app.get('/problems', async (req, res) => {
await problems.forEachAsync(async problem => {
problem.allowedEdit = await problem.isAllowedEditBy(res.locals.user);
problem.judge_state = await problem.getJudgeState(res.locals.user, true);
problem.tags = await problem.getTags();
});
res.render('problems', {
@ -46,6 +49,49 @@ app.get('/problems', async (req, res) => {
}
});
app.get('/problems/tag/:tagIDs', async (req, res) => {
try {
let tagIDs = Array.from(new Set(req.params.tagIDs.split(',').map(x => parseInt(x))));
let tags = await tagIDs.mapAsync(async tagID => ProblemTag.fromID(tagID));
// Validate the tagIDs
for (let tag of tags) {
if (!tag) {
return res.redirect(syzoj.utils.makeUrl(['problems']));
}
}
let sql = 'SELECT * FROM `problem` WHERE\n';
for (let tagID of tagIDs) {
if (tagID !== tagIDs[0]) {
sql += 'AND\n';
}
sql += '`problem`.`id` IN (SELECT `problem_id` FROM `problem_tag_map` WHERE `tag_id` = ' + tagID + ')';
}
let paginate = syzoj.utils.paginate(await Problem.count(sql), req.query.page, syzoj.config.page.problem);
let problems = await Problem.query(sql);
await problems.forEachAsync(async problem => {
problem.allowedEdit = await problem.isAllowedEditBy(res.locals.user);
problem.judge_state = await problem.getJudgeState(res.locals.user, true);
problem.tags = await problem.getTags();
});
res.render('problems', {
problems: problems,
tags: tags,
paginate: paginate
});
} catch (e) {
syzoj.log(e);
res.render('error', {
err: e
});
}
});
app.get('/problem/:id', async (req, res) => {
try {
let id = parseInt(req.params.id);
@ -66,6 +112,8 @@ app.get('/problem/:id', async (req, res) => {
let state = await problem.getJudgeState(res.locals.user, false);
problem.tags = await problem.getTags();
res.render('problem', {
problem: problem,
state: state
@ -80,7 +128,7 @@ app.get('/problem/:id', async (req, res) => {
app.get('/problem/:id/edit', async (req, res) => {
try {
let id = parseInt(req.params.id);
let id = parseInt(req.params.id) || 0;
let problem = await Problem.fromID(id);
if (!problem) {
@ -88,9 +136,11 @@ app.get('/problem/:id/edit', async (req, res) => {
problem = await Problem.create();
problem.id = id;
problem.allowedEdit = true;
problem.tags = [];
} else {
if (!await problem.isAllowedUseBy(res.locals.user)) throw 'Permission denied.';
problem.allowedEdit = await problem.isAllowedEditBy(res.locals.user);
problem.tags = await problem.getTags();
}
res.render('problem_edit', {
@ -124,8 +174,39 @@ app.post('/problem/:id/edit', async (req, res) => {
problem.example = req.body.example;
problem.limit_and_hint = req.body.limit_and_hint;
// Save the problem first, to have the `id` allocated
await problem.save();
if (!req.body.tags) {
req.body.tags = [];
} else if (!Array.isArray(req.body.tags)) {
req.body.tags = [req.body.tags];
}
let oldTagIDs = (await problem.getTags()).map(x => x.id);
let newTagIDs = await req.body.tags.map(x => parseInt(x)).filterAsync(async x => ProblemTag.fromID(x));
let delTagIDs = oldTagIDs.filter(x => !newTagIDs.includes(x));
let addTagIDs = newTagIDs.filter(x => !oldTagIDs.includes(x));
for (let tagID of delTagIDs) {
let map = await ProblemTagMap.findOne({ where: {
problem_id: id,
tag_id: tagID
} });
await map.destroy();
}
for (let tagID of addTagIDs) {
let map = await ProblemTagMap.create({
problem_id: id,
tag_id: tagID
});
await map.save();
}
res.redirect(syzoj.utils.makeUrl(['problem', problem.id]));
} catch (e) {
syzoj.log(e);

78
modules/problem_tag.js

@ -0,0 +1,78 @@
/*
* This file is part of SYZOJ.
*
* Copyright (c) 2016 Menci <huanghaorui301@gmail.com>
*
* SYZOJ is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* SYZOJ is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with SYZOJ. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';
let ProblemTag = syzoj.model('problem_tag');
app.get('/problems/tag/:id/edit', async (req, res) => {
try {
if (!res.locals.user && !res.locals.user.is_admin) throw 'Permission denied.';
let id = parseInt(req.params.id) || 0;
let tag = await ProblemTag.fromID(id);
if (!tag) {
tag = await ProblemTag.create();
tag.id = id;
}
res.render('problem_tag_edit', {
tag: tag
});
} catch (e) {
syzoj.log(e);
res.render('error', {
err: e
});
}
});
app.post('/problems/tag/:id/edit', async (req, res) => {
try {
if (!res.locals.user || !res.locals.user.is_admin) throw 'Permission denied.';
let id = parseInt(req.params.id) || 0;
let tag = await ProblemTag.fromID(id);
if (!tag) {
tag = await ProblemTag.create();
tag.id = id;
}
req.body.name = req.body.name.trim();
if (tag.name !== req.body.name) {
if (await ProblemTag.findOne({ where: { name: req.body.name } })) {
throw 'There is already a tag with that name.';
}
}
tag.name = req.body.name;
tag.color = req.body.color;
await tag.save();
res.redirect(syzoj.utils.makeUrl(['problems', 'tag', tag.id]));
} catch (e) {
syzoj.log(e);
res.render('error', {
err: e
});
}
});

5
utility.js

@ -23,6 +23,11 @@ Array.prototype.forEachAsync = Array.prototype.mapAsync = async function (fn) {
return Promise.all(this.map(fn));
};
Array.prototype.filterAsync = async function (fn) {
let a = await this.mapAsync(fn);
return this.filter((x, i) => a[i]);
};
let path = require('path');
let util = require('util');
let renderer = require('moemark-renderer');

4
views/contest_edit.ejs

@ -34,9 +34,9 @@ $(function () {
.dropdown({
debug: true,
apiSettings: {
url: '/api/v2/search/problem/{query}',
url: '/api/v2/search/problems/{query}',
onResponse: function (response) {
var a = $('#search_problems').val().map(x => parseInt(x));
var a = $('#search_problems').val().map(function (x) { return parseInt(x) });
if (response.results) {
response.results = response.results.filter(x => !a.includes(parseInt(x.value)));
}

25
views/problem.ejs

@ -116,6 +116,31 @@ if (contest) {
<div class="ui bottom attached segment"><%- problem.limit_and_hint %></div>
</div>
</div>
<% } %>
<% if (problem.tags && problem.tags.length) { %>
<div class="row">
<div class="column">
<h4 class="ui block header" id="show_tag_title_div" style="margin-bottom: 0; margin-left: -1px; margin-right: -1px; "><a href="#" id="show_tag_button" style="color: #000; ">显示分类标签</a></h4>
<div class="ui bottom attached segment" style="display: none; " id="show_tag_div">
<% for (let tag of problem.tags) { %>
<a href="<%= syzoj.utils.makeUrl(['problems', 'tag', tag.id]) %>" class="ui medium <%= tag.color %> label">
<%= tag.name %>
</a>
<% } %>
</div>
</div>
</div>
<script>
$(function () {
$('#show_tag_button').click(function (e) {
e.preventDefault();
$('#show_tag_title_div').addClass('top');
$('#show_tag_title_div').addClass('top attached');
$('#show_tag_title_div').text('分类标签');
$('#show_tag_div').css('display', '');
})
});
</script>
<% } %>
<% let noSubmit = false; %>
<%

41
views/problem_edit.ejs

@ -1,8 +1,4 @@
<% if (problem.id) {
this.title = '修改题目';
} else {
this.title = '添加题目';
} %>
<% this.title = '编辑题目'; %>
<% include header %>
<style type="text/css">
.problem_header{
@ -19,16 +15,22 @@
<div class="field">
<label for="title">题目名称</label>
<input type="text" id="title" name="title" value="<%= problem.title %>">
<label for="description">题目描述</label>
<label style="margin-top: 15px; " for="description">题目描述</label>
<textarea rows="15" id="description" name="description"><%= problem.description %></textarea>
<label for="input_format">输入格式</label>
<label style="margin-top: 15px; " for="input_format">输入格式</label>
<textarea rows="3" id="input" name="input_format"><%= problem.input_format %></textarea>
<label for="output_format">输出格式</label>
<label style="margin-top: 15px; " for="output_format">输出格式</label>
<textarea rows="3" id="output" name="output_format"><%= problem.output_format %></textarea>
<label>样例</label>
<label style="margin-top: 15px; ">样例</label>
<textarea rows="15" id="example" name="example"><%= problem.example %></textarea>
<label>数据范围与提示</label>
<label style="margin-top: 15px; ">数据范围与提示</label>
<textarea rows="10" id="hint" name="limit_and_hint"><%= problem.limit_and_hint %></textarea>
<label style="margin-top: 15px; ">标签</label>
<select class="ui fluid search dropdown" multiple="" id="search_tags" name="tags">
<% for (let tag of problem.tags) { %>
<option value="<%= tag.id %>" selected><%= tag.name %></option>
<% } %>
</select>
</div>
</div>
<div class="ui bottom attached tab segment" data-tab="preview" id="preview">
@ -69,4 +71,23 @@ $(function () {
$('.tabular.menu .item').tab();
});
</script>
<script>
$(function () {
$('#search_tags')
.dropdown({
debug: true,
apiSettings: {
url: '/api/v2/search/tags/{query}',
onResponse: function (response) {
var a = $('#search_tags').val().map(function (x) { return parseInt(x) });
if (response.results) {
response.results = response.results.filter(x => !a.includes(parseInt(x.value)));
}
return response;
},
cache: false
}
});
});
</script>
<% include footer %>

95
views/problems.ejs

@ -1,15 +1,72 @@
<% this.title = '题库' %>
<% include header %>
<%
let tagIDs = [];
if (typeof tags !== 'undefined') tagIDs = tags.map(x => x.id);
%>
<script src="//cdn.bootcss.com/js-cookie/2.1.4/js.cookie.min.js"></script>
<div class="padding">
<% if (user) { %>
<div class="ui grid">
<div class="row">
<div class="column">
<a href="<%= syzoj.utils.makeUrl(['problem', 0, 'edit']) %>" class="ui mini right floated button">添加题目</a>
<div class="ui grid">
<div class="row">
<div class="eight wide column">
<% if (typeof tags !== 'undefined') { %>
<%
tags.sort((a, b) => {
return a.name > b.name ? 1 : -1;
});
%>
<% for (let tag of tags) { %>
<% let tagListRemovedThisTag = tagIDs.filter(x => x != tag.id).sort().join(','); %>
<% let url = tagListRemovedThisTag ? syzoj.utils.makeUrl(['problems', 'tag', tagListRemovedThisTag]) : syzoj.utils.makeUrl(['problems']); %>
<a href="<%= url %>" class="ui tiny <%= tag.color %> label">
<%= tag.name %>
</a>
<% } %>
<% } %>
</div>
<div class="eight wide right aligned column">
<div class="ui toggle checkbox" id="show_tag">
<style id="show_tag_style"></style>
<script>
if (Cookies.get('show_tag') === '1') {
document.write('<input type="checkbox" checked>');
document.getElementById('show_tag_style').innerHTML = '.show_tag_controled { visibility: visible; }';
} else {
document.write('<input type="checkbox">');
document.getElementById('show_tag_style').innerHTML = '.show_tag_controled { visibility: hidden; }';
}
</script>
<script>
$(function () {
$('#show_tag').checkbox('setting', 'onChange', function () {
let checked = $('#show_tag').checkbox('is checked');
Cookies.set('show_tag', checked ? '1' : '0');
if (checked) {
document.getElementById('show_tag_style').innerHTML = '.show_tag_controled { visibility: visible; }';
} else {
document.getElementById('show_tag_style').innerHTML = '.show_tag_controled { visibility: hidden; }';
}
});
});
</script>
<label>显示分类标签</label>
</div>
<div style="margin-left: 10px; display: inline-block; ">
<% if (user && user.is_admin) { %>
<% if (typeof tags !== 'undefined' && tags.length === 1) { %>
<a style="margin-left: 10px; " href="<%= syzoj.utils.makeUrl(['problems', 'tag', tags[0].id, 'edit']) %>" class="ui mini blue button">编辑标签</a>
<% } %>
<a style="margin-left: 10px; " href="<%= syzoj.utils.makeUrl(['problems', 'tag', 0, 'edit']) %>" class="ui mini green button">添加标签</a>
<% } %>
<% if (user) { %>
<a style="margin-left: 10px; " href="<%= syzoj.utils.makeUrl(['problem', 0, 'edit']) %>" class="ui mini button">添加题目</a>
<% } %>
</div>
</div>
</div>
<% } %>
</div>
<table class="ui very basic center aligned table">
<thead>
<tr>
@ -18,6 +75,7 @@
<% } %>
<th class="one wide">编号</th>
<th class="left aligned">题目名称</th>
<th class="right aligned">&nbsp;</th>
<th class="one wide">通过</th>
<th class="one wide">提交</th>
<th class="one wide">通过率</th>
@ -40,8 +98,31 @@
<% } %>
<td><%= problem.id %></td>
<td class="left aligned"><a href="<%= syzoj.utils.makeUrl(['problem', problem.id]) %>"><%= problem.title %>
<% if (!problem.is_public) { %><span class="ui header"><span class="ui tiny red label">未公开</span></span><% } %>
<% if (!problem.is_public) { %><span class="ui header"><span class="ui tiny red label">未公开</span></span><% } %>
</a></td>
<td class="right aligned">
<div class="show_tag_controled">
<%
if (problem.tags) {
for (let tag of problem.tags) {
let tagListToggledThisTag;
if (!tagIDs.includes(tag.id)) tagListToggledThisTag = tagIDs.concat([tag.id]);
else tagListToggledThisTag = tagIDs.filter(x => x != tag.id);
tagListToggledThisTag = tagListToggledThisTag.sort().join(',');
let url = tagListToggledThisTag ? syzoj.utils.makeUrl(['problems', 'tag', tagListToggledThisTag]) : syzoj.utils.makeUrl(['problems']);
%>
<span class="ui header">
<a href="<%= url %>" class="ui tiny <%= tag.color %> label">
<%= tag.name %>
</a>
</span>
<%
}
}
%>
</div>
</td>
<td><%= problem.ac_num %></td>
<td><%= problem.submit_num %></td>
<td><%= ((problem.ac_num / problem.submit_num * 100) || 0).toFixed(2) %>%</td>

Loading…
Cancel
Save