Browse Source

Finish contest submission list.

pull/6/head
t123yh 7 years ago
parent
commit
2c757e2940
  1. 1
      app.js
  2. 17
      libs/judger.js
  3. 40
      libs/submissions_process.js
  4. 25
      models/contest.js
  5. 49
      models/judge_state.js
  6. 104
      modules/contest.js
  7. 13
      modules/problem.js
  8. 147
      modules/submission.js
  9. 16
      views/contest.ejs
  10. 108
      views/contest_submissions.ejs
  11. 3
      views/submission_content.ejs
  12. 37
      views/submissions.ejs
  13. 38
      views/submissions_item.ejs

1
app.js

@ -10,7 +10,6 @@
* *
* SYZOJ is distributed in the hope that it will be useful, * SYZOJ is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * 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. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public * You should have received a copy of the GNU Affero General Public

17
libs/judger.js

@ -2,9 +2,12 @@ const enums = require('./enums'),
rp = require('request-promise'), rp = require('request-promise'),
url = require('url'); url = require('url');
module.exports.judge = async function (judge_state, priority) { const util = require('util');
module.exports.judge = async function (judge_state, problem, priority) {
let type, param, extraFile = null; let type, param, extraFile = null;
switch (judge_state.problem.type) { console.log("JudgeState: " + util.inspect(judge_state));
console.log("Problem: " + util.inspect(judge_state));
switch (problem.type) {
case 'submit-answer': case 'submit-answer':
type = enums.ProblemType.AnswerSubmission; type = enums.ProblemType.AnswerSubmission;
param = null; param = null;
@ -18,10 +21,10 @@ module.exports.judge = async function (judge_state, priority) {
param = { param = {
language: judge_state.language, language: judge_state.language,
code: judge_state.code, code: judge_state.code,
timeLimit: judge_state.problem.time_limit, timeLimit: problem.time_limit,
memoryLimit: judge_state.problem.memory_limit, memoryLimit: problem.memory_limit,
fileIOInput: judge_state.problem.file_io ? judge_state.problem.file_io_input_name : null, fileIOInput: problem.file_io ? problem.file_io_input_name : null,
fileIOOutput: judge_state.problem.file_io ? judge_state.problem.file_io_output_name : null fileIOOutput: problem.file_io ? problem.file_io_output_name : null
}; };
break; break;
} }
@ -29,7 +32,7 @@ module.exports.judge = async function (judge_state, priority) {
const req = { const req = {
content: { content: {
taskId: judge_state.id, taskId: judge_state.id,
testData: judge_state.problem.id.toString(), testData: problem.id.toString(),
type: type, type: type,
priority: priority, priority: priority,
param: param param: param

40
libs/submissions_process.js

@ -0,0 +1,40 @@
const getSubmissionInfo = (s, displayConfig) => ({
taskId: s.id,
user: s.user.username,
userId: s.user_id,
problemName: s.problem.title,
problemId: s.problem_id,
language: displayConfig.hideCode ? null : ((s.language != null && s.language !== '') ? syzoj.config.languages[s.language].show : null),
codeSize: displayConfig.hideCode ? null : syzoj.utils.formatSize(s.code.length),
submitTime: syzoj.utils.formatDate(s.submit_time),
});
const getRoughResult = (x, displayConfig) => {
if (displayConfig.hideResult) {
// 0: Waiting 1: Running
if (x.status === "System Error")
return { result: "System Error" };
if (x.compilation == null || [0, 1].includes(x.compilation.status)) {
return null;
} else {
if (x.compilation.status === 2) { // 2 is TaskStatus.Done
return { result: "Submitted" };
} else {
return { result: "Compile Error" };
}
}
} else {
if (x.pending) {
return null;
} else {
return {
result: x.status,
time: displayConfig.hideUsage ? null : x.total_time,
memory: displayConfig.hideUsage ? null : x.max_memory,
score: displayConfig.hideUsage ? null : x.score
};
}
}
}
module.exports = { getRoughResult, getSubmissionInfo };

25
models/contest.js

@ -91,13 +91,28 @@ class Contest extends Model {
this.ranklist = await ContestRanklist.fromID(this.ranklist_id); this.ranklist = await ContestRanklist.fromID(this.ranklist_id);
} }
async isAllowedEditBy(user) { async isSupervisior(user) {
return user && (user.is_admin || this.holder_id === user.id); return user && (user.is_admin || this.holder_id === user.id);
} }
async isAllowedSeeResultBy(user) { allowedSeeingOthers() {
if (this.type === 'acm') return true; if (this.type === 'acm') return true;
return (user && (user.is_admin || this.holder_id === user.id)) || !(await this.isRunning()); else return false;
}
allowedSeeingScore() { // If not, then the user can only see status
if (this.type === 'ioi') return true;
else return false;
}
allowedSeeingResult() { // If not, then the user can only see compile progress
if (this.type === 'ioi' || this.type === 'acm') return true;
else return false;
}
allowedSeeingTestcase() {
if (this.type === 'ioi') return true;
return false;
} }
async getProblems() { async getProblems() {
@ -145,12 +160,12 @@ class Contest extends Model {
}); });
} }
async isRunning(now) { isRunning(now) {
if (!now) now = syzoj.utils.getCurrentDate(); if (!now) now = syzoj.utils.getCurrentDate();
return now >= this.start_time && now < this.end_time; return now >= this.start_time && now < this.end_time;
} }
async isEnded(now) { isEnded(now) {
if (!now) now = syzoj.utils.getCurrentDate(); if (!now) now = syzoj.utils.getCurrentDate();
return now >= this.end_time; return now >= this.end_time;
} }

49
models/judge_state.js

@ -116,7 +116,7 @@ class JudgeState extends Model {
else if (this.type === 0) return this.problem.is_public || (user && (await user.hasPrivilege('manage_problem'))); else if (this.type === 0) return this.problem.is_public || (user && (await user.hasPrivilege('manage_problem')));
else if (this.type === 1) { else if (this.type === 1) {
let contest = await Contest.fromID(this.type_info); let contest = await Contest.fromID(this.type_info);
if (await contest.isRunning()) { if (contest.isRunning()) {
return (user && this.user_id === user.id) || (user && user.is_admin); return (user && this.user_id === user.id) || (user && user.is_admin);
} else { } else {
return true; return true;
@ -124,51 +124,6 @@ class JudgeState extends Model {
} }
} }
async isAllowedSeeCodeBy(user) {
await this.loadRelationships();
if (user && user.id === this.problem.user_id) return true;
else if (this.type === 0) return this.problem.is_public || (user && (await user.hasPrivilege('manage_problem')));
else if (this.type === 1) {
let contest = await Contest.fromID(this.type_info);
if (await contest.isRunning()) {
return (user && this.user_id === user.id) || (user && user.is_admin);
} else {
return true;
}
}
}
async isAllowedSeeCaseBy(user) {
await this.loadRelationships();
if (user && user.id === this.problem.user_id) return true;
else if (this.type === 0) return this.problem.is_public || (user && (await user.hasPrivilege('manage_problem')));
else if (this.type === 1) {
let contest = await Contest.fromID(this.type_info);
if (await contest.isRunning()) {
return contest.type === 'ioi' || (user && user.is_admin);
} else {
return true;
}
}
}
async isAllowedSeeDataBy(user) {
await this.loadRelationships();
if (user && user.id === this.problem.user_id) return true;
else if (this.type === 0) return this.problem.is_public || (user && (await user.hasPrivilege('manage_problem')));
else if (this.type === 1) {
let contest = await Contest.fromID(this.type_info);
if (await contest.isRunning()) {
return user && user.is_admin;
} else {
return true;
}
}
}
async updateRelatedInfo(newSubmission) { async updateRelatedInfo(newSubmission) {
await syzoj.utils.lock(['JudgeState::updateRelatedInfo', 'problem', this.problem_id], async () => { await syzoj.utils.lock(['JudgeState::updateRelatedInfo', 'problem', this.problem_id], async () => {
await syzoj.utils.lock(['JudgeState::updateRelatedInfo', 'user', this.user_id], async () => { await syzoj.utils.lock(['JudgeState::updateRelatedInfo', 'user', this.user_id], async () => {
@ -237,7 +192,7 @@ class JudgeState extends Model {
await contest.newSubmission(this); await contest.newSubmission(this);
} }
await Judger.judge(this, 1); await Judger.judge(this, this.problem, 1);
}); });
} }

104
modules/contest.js

@ -26,6 +26,9 @@ let Problem = syzoj.model('problem');
let JudgeState = syzoj.model('judge_state'); let JudgeState = syzoj.model('judge_state');
let User = syzoj.model('user'); let User = syzoj.model('user');
const jwt = require('jsonwebtoken');
const { getSubmissionInfo, getRoughResult } = require('../libs/submissions_process');
app.get('/contests', async (req, res) => { app.get('/contests', async (req, res) => {
try { try {
let where; let where;
@ -118,15 +121,15 @@ app.post('/contest/:id/edit', async (req, res) => {
app.get('/contest/:id', async (req, res) => { app.get('/contest/:id', async (req, res) => {
try { try {
const curUser = res.locals.user;
let contest_id = parseInt(req.params.id); let contest_id = parseInt(req.params.id);
let contest = await Contest.fromID(contest_id); let contest = await Contest.fromID(contest_id);
if (!contest) throw new ErrorMessage('无此比赛。'); if (!contest) throw new ErrorMessage('无此比赛。');
if (!contest.is_public && (!res.locals.user || !res.locals.user.is_admin)) throw new ErrorMessage('无此比赛。'); if (!contest.is_public && (!res.locals.user || !res.locals.user.is_admin)) throw new ErrorMessage('无此比赛。');
contest.allowedEdit = await contest.isAllowedEditBy(res.locals.user); contest.running = contest.isRunning();
contest.running = await contest.isRunning(); contest.ended = contest.isEnded();
contest.ended = await contest.isEnded();
contest.subtitle = await syzoj.utils.markdown(contest.subtitle); contest.subtitle = await syzoj.utils.markdown(contest.subtitle);
contest.information = await syzoj.utils.markdown(contest.information); contest.information = await syzoj.utils.markdown(contest.information);
@ -205,7 +208,8 @@ app.get('/contest/:id', async (req, res) => {
res.render('contest', { res.render('contest', {
contest: contest, contest: contest,
problems: problems, problems: problems,
hasStatistics: hasStatistics hasStatistics: hasStatistics,
isSupervisior: await contest.isSupervisior(curUser)
}); });
} catch (e) { } catch (e) {
syzoj.log(e); syzoj.log(e);
@ -262,28 +266,48 @@ app.get('/contest/:id/submissions', async (req, res) => {
try { try {
let contest_id = parseInt(req.params.id); let contest_id = parseInt(req.params.id);
let contest = await Contest.fromID(contest_id); let contest = await Contest.fromID(contest_id);
if (!contest) throw new ErrorMessage('无此比赛。'); if (!contest) throw new ErrorMessage('无此比赛。');
if (!await contest.isAllowedSeeResultBy(res.locals.user)) throw new ErrorMessage('您没有权限进行此操作。');
contest.ended = await contest.isEnded();
if (contest.isEnded()) {
res.redirect(syzoj.utils.makeUrl(['submissions'], { contest: contest_id }));
return;
}
const hideResult = !contest.allowedSeeingResult();
const displayConfig = {
pushType: hideResult ? 'compile' : 'rough',
hideScore: !contest.allowedSeeingScore(),
hideUsage: true,
hideCode: true,
hideResult: hideResult,
hideOthers: !contest.allowedSeeingOthers(),
showDetailResult: contest.allowedSeeingTestcase(),
inContest: true
};
let problems_id = await contest.getProblems(); let problems_id = await contest.getProblems();
const curUser = res.locals.user;
let user = await User.fromName(req.query.submitter || ''); let user = req.query.submitter && await User.fromName(req.query.submitter);
let where = {}; let where = {};
if (user) where.user_id = user.id; if (displayConfig.hideOthers) {
if (req.query.problem_id) where.problem_id = problems_id[parseInt(req.query.problem_id) - 1]; if (curUser == null || // Not logined
where.type = 1; (user && user.id !== curUser.id)) { // Not querying himself
where.type_info = contest_id; throw new ErrorMessage("您没有权限执行此操作");
}
where.user_id = curUser.id;
} else {
if (user) {
where.user_id = user.id;
}
}
if (contest.ended || (res.locals.user && res.locals.user.is_admin)) { if (!displayConfig.hideScore) {
if (!((!res.locals.user || !res.locals.user.is_admin) && !contest.ended && contest.type === 'acm')) {
let minScore = parseInt(req.query.min_score); let minScore = parseInt(req.query.min_score);
if (isNaN(minScore)) minScore = 0;
let maxScore = parseInt(req.query.max_score); let maxScore = parseInt(req.query.max_score);
if (isNaN(maxScore)) maxScore = 100;
if (!isNaN(minScore) || !isNaN(maxScore)) {
if (isNaN(minScore)) minScore = 0;
if (isNaN(maxScore)) maxScore = 100;
where.score = { where.score = {
$and: { $and: {
$gte: parseInt(minScore), $gte: parseInt(minScore),
@ -291,32 +315,44 @@ app.get('/contest/:id/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 (!displayConfig.hideResult) {
if (req.query.status) where.status = { $like: req.query.status + '%' }; if (req.query.status) where.status = { $like: req.query.status + '%' };
} }
if (req.query.problem_id) where.problem_id = problems_id[parseInt(req.query.problem_id) - 1];
where.type = 1;
where.type_info = contest_id;
let paginate = syzoj.utils.paginate(await JudgeState.count(where), req.query.page, syzoj.config.page.judge_state); 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']]); let judge_state = await JudgeState.query(paginate, where, [['submit_time', 'desc']]);
await judge_state.forEachAsync(async obj => obj.allowedSeeCode = await obj.isAllowedSeeCodeBy(res.locals.user));
await judge_state.forEachAsync(async obj => { await judge_state.forEachAsync(async obj => {
await obj.loadRelationships(); await obj.loadRelationships();
obj.problem_id = problems_id.indexOf(obj.problem_id) + 1; obj.problem_id = problems_id.indexOf(obj.problem_id) + 1;
obj.problem.title = syzoj.utils.removeTitleTag(obj.problem.title); obj.problem.title = syzoj.utils.removeTitleTag(obj.problem.title);
if (contest.type === 'noi' && !contest.ended && !await obj.problem.isAllowedEditBy(res.locals.user)) {
if (!['Compile Error', 'Waiting', 'Compiling'].includes(obj.status)) {
obj.status = 'Submitted';
}
}
}); });
res.render('contest_submissions', { res.render('submissions', {
contest: contest, contest: contest,
judge_state: judge_state, items: judge_state.map(x => ({
info: getSubmissionInfo(x, displayConfig),
token: (getRoughResult(x, displayConfig) == null) ? jwt.sign({
taskId: x.id,
displayConfig: displayConfig
}, syzoj.config.judge_token) : null,
result: getRoughResult(x, displayConfig),
running: false,
})),
paginate: paginate, paginate: paginate,
form: req.query form: req.query,
displayConfig: displayConfig,
}); });
} catch (e) { } catch (e) {
syzoj.log(e); syzoj.log(e);
@ -326,7 +362,7 @@ app.get('/contest/:id/submissions', async (req, res) => {
} }
}); });
app.get('/contest/:id/:pid', async (req, res) => { app.get('/contest/:id/problem/:pid', async (req, res) => {
try { try {
let contest_id = parseInt(req.params.id); let contest_id = parseInt(req.params.id);
let contest = await Contest.fromID(contest_id); let contest = await Contest.fromID(contest_id);
@ -341,8 +377,8 @@ app.get('/contest/:id/:pid', async (req, res) => {
let problem = await Problem.fromID(problem_id); let problem = await Problem.fromID(problem_id);
await problem.loadRelationships(); await problem.loadRelationships();
contest.ended = await contest.isEnded(); contest.ended = contest.isEnded();
if (!(await contest.isRunning() || contest.ended)) { if (!(contest.isRunning() || contest.isEnded())) {
if (await problem.isAllowedUseBy(res.locals.user)) { if (await problem.isAllowedUseBy(res.locals.user)) {
return res.redirect(syzoj.utils.makeUrl(['problem', problem_id])); return res.redirect(syzoj.utils.makeUrl(['problem', problem_id]));
} }
@ -351,7 +387,7 @@ app.get('/contest/:id/:pid', async (req, res) => {
problem.specialJudge = await problem.hasSpecialJudge(); problem.specialJudge = await problem.hasSpecialJudge();
await syzoj.utils.markdown(problem, [ 'description', 'input_format', 'output_format', 'example', 'limit_and_hint' ]); await syzoj.utils.markdown(problem, ['description', 'input_format', 'output_format', 'example', 'limit_and_hint']);
let state = await problem.getJudgeState(res.locals.user, false); let state = await problem.getJudgeState(res.locals.user, false);
let testcases = await syzoj.utils.parseTestdata(problem.getTestdataPath(), problem.type === 'submit-answer'); let testcases = await syzoj.utils.parseTestdata(problem.getTestdataPath(), problem.type === 'submit-answer');
@ -388,8 +424,8 @@ app.get('/contest/:id/:pid/download/additional_file', async (req, res) => {
let problem_id = problems_id[pid - 1]; let problem_id = problems_id[pid - 1];
let problem = await Problem.fromID(problem_id); let problem = await Problem.fromID(problem_id);
contest.ended = await contest.isEnded(); contest.ended = contest.isEnded();
if (!(await contest.isRunning() || contest.ended)) { if (!(contest.isRunning() || contest.idEnded())) {
if (await problem.isAllowedUseBy(res.locals.user)) { if (await problem.isAllowedUseBy(res.locals.user)) {
return res.redirect(syzoj.utils.makeUrl(['problem', problem_id, 'download', 'additional_file'])); return res.redirect(syzoj.utils.makeUrl(['problem', problem_id, 'download', 'additional_file']));
} }

13
modules/problem.js

@ -618,11 +618,11 @@ app.post('/problem/:id/submit', app.multer.fields([{ name: 'answer', maxCount: 1
}); });
} }
let contest_id = parseInt(req.query.contest_id), redirectToContest = false; let contest_id = parseInt(req.query.contest_id);
if (contest_id) { if (contest_id) {
let contest = await Contest.fromID(contest_id); let contest = await Contest.fromID(contest_id);
if (!contest) throw new ErrorMessage('无此比赛。'); if (!contest) throw new ErrorMessage('无此比赛。');
if (!await contest.isRunning()) throw new ErrorMessage('比赛未开始或已结束。'); if (!contest.isRunning()) throw new ErrorMessage('比赛未开始或已结束。');
let problems_id = await contest.getProblems(); let problems_id = await contest.getProblems();
if (!problems_id.includes(id)) throw new ErrorMessage('无此题目。'); if (!problems_id.includes(id)) throw new ErrorMessage('无此题目。');
@ -638,14 +638,19 @@ app.post('/problem/:id/submit', app.multer.fields([{ name: 'answer', maxCount: 1
await judge_state.updateRelatedInfo(true); await judge_state.updateRelatedInfo(true);
try { try {
await Judger.judge(judge_state, contest_id ? 3 : 2); await Judger.judge(judge_state, problem, contest_id ? 3 : 2);
} catch (err) { } catch (err) {
judge_state.status = "System Error"; judge_state.status = "System Error";
judge_state.pending = false;
await judge_state.save(); await judge_state.save();
throw new ErrorMessage(`无法开始评测:${err.toString()}`); throw new ErrorMessage(`无法开始评测:${err.toString()}`);
} }
if (contest_id) {
res.redirect(syzoj.utils.makeUrl(['contest', contest_id, 'submissions']));
} else {
res.redirect(syzoj.utils.makeUrl(['submission', judge_state.id])); res.redirect(syzoj.utils.makeUrl(['submission', judge_state.id]));
}
} catch (e) { } catch (e) {
syzoj.log(e); syzoj.log(e);
res.render('error', { res.render('error', {
@ -781,7 +786,7 @@ app.get('/problem/:id/download/additional_file', async (req, res) => {
if (contest_id) { if (contest_id) {
let contest = await Contest.fromID(contest_id); let contest = await Contest.fromID(contest_id);
if (!contest) throw new ErrorMessage('无此比赛。'); if (!contest) throw new ErrorMessage('无此比赛。');
if (!await contest.isRunning()) throw new ErrorMessage('比赛未开始或已结束。'); if (!contest.isRunning()) throw new ErrorMessage('比赛未开始或已结束。');
let problems_id = await contest.getProblems(); let problems_id = await contest.getProblems();
if (!problems_id.includes(id)) throw new ErrorMessage('无此题目。'); if (!problems_id.includes(id)) throw new ErrorMessage('无此题目。');
} else { } else {

147
modules/submission.js

@ -24,33 +24,33 @@ let User = syzoj.model('user');
let Contest = syzoj.model('contest'); let Contest = syzoj.model('contest');
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const { getSubmissionInfo, getRoughResult } = require('../libs/submissions_process');
// s is JudgeState // s is JudgeState
const getSubmissionInfo = (s) => ({
taskId: s.id,
user: s.user.username,
userId: s.user_id,
problemName: s.problem.title,
problemId: s.problem.id,
language: (s.language != null && s.language !== '') ? syzoj.config.languages[s.language].show : null,
codeSize: s.allowedSeeCode ? syzoj.utils.formatSize(s.code.length) : null,
submitTime: syzoj.utils.formatDate(s.submit_time),
});
const getRoughResult = (x) => (x.pending ? null : {
result: x.status,
time: x.total_time,
memory: x.max_memory,
score: x.score
});
app.get('/submissions', async (req, res) => { app.get('/submissions', async (req, res) => {
try { try {
const curUser = res.locals.user;
let user = await User.fromName(req.query.submitter || ''); let user = await User.fromName(req.query.submitter || '');
let where = {}; let where = {};
if (user) where.user_id = user.id; if (user) where.user_id = user.id;
else if (req.query.submitter) where.user_id = -1; else if (req.query.submitter) where.user_id = -1;
if (req.query.contest == null) {
where.type = { $ne: 1 };
} else {
const contestId = Number(req.query.contest);
const contest = await Contest.fromID(contestId);
contest.ended = contest.isEnded();
if (contest.ended || // If the contest is ended
(curUser && contest.isSupervisior(curUser)) // Or if the user have the permission to check
) {
where.type = { $eq: 1 };
where.type_info = { $eq: contestId };
} else {
throw new Error("您暂时无权查看此比赛的详细评测信息。");
}
}
let minScore = parseInt(req.query.min_score); let minScore = parseInt(req.query.min_score);
let maxScore = parseInt(req.query.max_score); let maxScore = parseInt(req.query.max_score);
@ -71,9 +71,7 @@ app.get('/submissions', async (req, res) => {
} }
if (req.query.status) where.status = { $like: req.query.status + '%' }; if (req.query.status) where.status = { $like: req.query.status + '%' };
where.type = { $ne: 1 }; if (!curUser || !await curUser.hasPrivilege('manage_problem')) {
if (!res.locals.user || !await res.locals.user.hasPrivilege('manage_problem')) {
if (req.query.problem_id) { if (req.query.problem_id) {
where.problem_id = { where.problem_id = {
$and: [ $and: [
@ -94,22 +92,31 @@ app.get('/submissions', async (req, res) => {
let judge_state = await JudgeState.query(paginate, where, [['submit_time', 'desc']]); let judge_state = await JudgeState.query(paginate, where, [['submit_time', 'desc']]);
await judge_state.forEachAsync(async obj => obj.loadRelationships()); await judge_state.forEachAsync(async obj => obj.loadRelationships());
await judge_state.forEachAsync(async obj => obj.allowedSeeCode = await obj.isAllowedSeeCodeBy(res.locals.user));
await judge_state.forEachAsync(async obj => obj.allowedSeeData = await obj.isAllowedSeeDataBy(res.locals.user));
const displayConfig = {
pushType: 'rough',
hideScore: false,
hideUsage: false,
hideCode: false,
hideResult: false,
hideOthers: false,
inContest: false,
showDetailResult: true
};
res.render('submissions', { res.render('submissions', {
// judge_state: judge_state, // judge_state: judge_state,
items: judge_state.map(x => ({ items: judge_state.map(x => ({
info: getSubmissionInfo(x), info: getSubmissionInfo(x, displayConfig),
token: x.pending ? jwt.sign({ token: (getRoughResult(x, displayConfig) == null) ? jwt.sign({
taskId: x.id, taskId: x.id,
type: 'rough' displayConfig: displayConfig
}, syzoj.config.judge_token) : null, }, syzoj.config.judge_token) : null,
result: getRoughResult(x), result: getRoughResult(x, displayConfig),
running: false running: false,
})), })),
paginate: paginate, paginate: paginate,
form: req.query form: req.query,
displayConfig: displayConfig,
}); });
} catch (e) { } catch (e) {
syzoj.log(e); syzoj.log(e);
@ -128,7 +135,14 @@ app.get('/submission/:id', async (req, res) => {
let contest; let contest;
if (judge.type === 1) { if (judge.type === 1) {
contest = await Contest.fromID(judge.type_info); contest = await Contest.fromID(judge.type_info);
contest.ended = await contest.isEnded(); contest.ended = contest.isEnded();
let problems_id = await contest.getProblems();
judge.problem_id = problems_id.indexOf(judge.problem_id) + 1;
judge.problem.title = syzoj.utils.removeTitleTag(judge.problem.title);
if (!contest.ended && !await judge.problem.isAllowedEditBy(res.locals.user)) {
throw new Error("对不起,在比赛结束之前,您不能查看评测结果。");
}
} }
await judge.loadRelationships(); await judge.loadRelationships();
@ -137,31 +151,14 @@ app.get('/submission/:id', async (req, res) => {
judge.codeLength = judge.code.length; judge.codeLength = judge.code.length;
judge.code = await syzoj.utils.highlight(judge.code, syzoj.config.languages[judge.language].highlight); 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);
judge.allowedRejudge = await judge.problem.isAllowedEditBy(res.locals.user); judge.allowedRejudge = await judge.problem.isAllowedEditBy(res.locals.user);
judge.allowedManage = await judge.problem.isAllowedManageBy(res.locals.user); judge.allowedManage = await judge.problem.isAllowedManageBy(res.locals.user);
let hideScore = false;
if (contest) {
let problems_id = await contest.getProblems();
judge.problem_id = problems_id.indexOf(judge.problem_id) + 1;
judge.problem.title = syzoj.utils.removeTitleTag(judge.problem.title);
if (contest.type === 'noi' && !contest.ended && !await judge.problem.isAllowedEditBy(res.locals.user)) {
if (!['Compile Error', 'Waiting', 'Compiling'].includes(judge.status)) {
judge.status = 'Submitted';
}
hideScore = true;
}
}
res.render('submission', { res.render('submission', {
info: getSubmissionInfo(judge), info: getSubmissionInfo(judge),
roughResult: getRoughResult(judge), roughResult: getRoughResult(judge),
code: judge.code.toString("utf8"), code: (judge.problem.type !== 'submit-answer') ? judge.code.toString("utf8") : '',
detailResult: judge.result, detailResult: judge.result,
socketToken: judge.pending ? jwt.sign({ socketToken: judge.pending ? jwt.sign({
taskId: judge.id, taskId: judge.id,
@ -176,58 +173,6 @@ app.get('/submission/:id', async (req, res) => {
} }
}); });
app.get('/submission/:id/ajax', async (req, res) => {
try {
let id = parseInt(req.params.id);
let judge = await JudgeState.fromID(id);
if (!judge || !await judge.isAllowedVisitBy(res.locals.user)) throw new ErrorMessage('您没有权限进行此操作。');
let contest;
if (judge.type === 1) {
contest = await Contest.fromID(judge.type_info);
contest.ended = await contest.isEnded();
}
await judge.loadRelationships();
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);
judge.allowedRejudge = await judge.problem.isAllowedEditBy(res.locals.user);
judge.allowedManage = await judge.problem.isAllowedManageBy(res.locals.user);
let hideScore = false;
if (contest) {
let problems_id = await contest.getProblems();
judge.problem_id = problems_id.indexOf(judge.problem_id) + 1;
judge.problem.title = syzoj.utils.removeTitleTag(judge.problem.title);
if (contest.type === 'noi' && !contest.ended && !await judge.problem.isAllowedEditBy(res.locals.user)) {
if (!['Compile Error', 'Waiting', 'Compiling'].includes(judge.status)) {
judge.status = 'Submitted';
}
hideScore = true;
}
}
res.render('submission_content', {
hideScore, hideScore,
contest: contest,
judge: judge
});
} catch (e) {
syzoj.log(e);
res.render('error', {
err: e
});
}
});
app.post('/submission/:id/rejudge', async (req, res) => { app.post('/submission/:id/rejudge', async (req, res) => {
try { try {
let id = parseInt(req.params.id); let id = parseInt(req.params.id);

16
views/contest.ejs

@ -10,7 +10,8 @@
<div class="padding"> <div class="padding">
<h1><%= contest.title %></h1> <h1><%= contest.title %></h1>
<div style="margin-bottom: 30px;"><%- contest.subtitle %></div> <div style="margin-bottom: 30px;"><%- contest.subtitle %></div>
<% let unveiled = (contest.allowedEdit || syzoj.utils.getCurrentDate() >= contest.start_time); %> <% let unveiled = (isSupervisior || syzoj.utils.getCurrentDate() >= contest.start_time); %>
<% const seeResult = (isSupervisior || contest.ended) %>
<% let start = syzoj.utils.formatDate(contest.start_time), end = syzoj.utils.formatDate(contest.end_time); %> <% let start = syzoj.utils.formatDate(contest.start_time), end = syzoj.utils.formatDate(contest.end_time); %>
<% if (contest.running && start.split(' ')[0] === end.split(' ')[0]) { <% if (contest.running && start.split(' ')[0] === end.split(' ')[0]) {
start = start.split(' ')[1]; end = end.split(' ')[1]; start = start.split(' ')[1]; end = end.split(' ')[1];
@ -22,19 +23,22 @@
<div class="bar" style="width: <%= timePercentage %>%;"></div> <div class="bar" style="width: <%= timePercentage %>%;"></div>
</div> </div>
<div class="ui grid"> <div class="ui grid">
<% if (contest.allowedEdit || (unveiled && (!contest.running || contest.type === 'acm'))) { %>
<div class="row"> <div class="row">
<div class="column"> <div class="column">
<div class="ui buttons"> <div class="ui buttons">
<% if(seeResult) { %>
<a class="ui small blue button" href="<%= syzoj.utils.makeUrl(['contest', contest.id, 'ranklist']) %>">排行榜</a> <a class="ui small blue button" href="<%= syzoj.utils.makeUrl(['contest', contest.id, 'ranklist']) %>">排行榜</a>
<a class="ui small positive button" href="<%= syzoj.utils.makeUrl(['contest', contest.id, 'submissions']) %>">提交记录</a> <% } %>
<% if (contest.allowedEdit) { %> <% let submissionsUrl = seeResult ?
syzoj.utils.makeUrl(['submissions'], {contest: contest.id}) :
syzoj.utils.makeUrl(['contest', contest.id, 'submissions']); %>
<a class="ui small positive button" href="<%= submissionsUrl %>">提交记录</a>
<% if (isSupervisior) { %>
<a class="ui small button" href="<%= syzoj.utils.makeUrl(['contest', contest.id, 'edit']) %>">编辑比赛</a> <a class="ui small button" href="<%= syzoj.utils.makeUrl(['contest', contest.id, 'edit']) %>">编辑比赛</a>
<% } %> <% } %>
</div> </div>
</div> </div>
</div> </div>
<% } %>
<% if (contest.information) { %> <% if (contest.information) { %>
<div class="row"> <div class="row">
<div class="column"> <div class="column">
@ -92,7 +96,7 @@
<% } %> <% } %>
<% } %> <% } %>
</td> </td>
<td><a href="<%= syzoj.utils.makeUrl(['contest', contest.id, i]) %>"><%= syzoj.utils.removeTitleTag(problem.problem.title) %></a></td> <td><a href="<%= syzoj.utils.makeUrl(['contest', contest.id, 'problem', i]) %>"><%= syzoj.utils.removeTitleTag(problem.problem.title) %></a></td>
<% if (hasStatistics) { %> <% if (hasStatistics) { %>
<td class="center aligned" style="white-space: nowrap; "> <td class="center aligned" style="white-space: nowrap; ">
<a href="<%= syzoj.utils.makeUrl(['contest', contest.id, 'submissions'], { problem_id: i, status: 'Accepted' }) %>"><%= problem.statistics.accepted %></a> <a href="<%= syzoj.utils.makeUrl(['contest', contest.id, 'submissions'], { problem_id: i, status: 'Accepted' }) %>"><%= problem.statistics.accepted %></a>

108
views/contest_submissions.ejs

@ -1,108 +0,0 @@
<% this.title = '提交记录 - ' + contest.title %>
<% include header %>
<script src="/textFit.js"></script>
<div class="padding">
<form action="<%= syzoj.utils.makeUrl(['contest', contest.id, 'submissions']) %>" class="ui mini form" method="get" role="form" id="form" onsubmit="return checkSubmit()">
<div class="inline fields" style="margin-bottom: 25px; ">
<label style="font-size: 1.2em; margin-right: 1px; ">题目:</label>
<div class="field"><input id="problem_id" style="width: 50px; " type="text" value="<%= this.alpha(form.problem_id) %>"></div>
<input type="hidden" name="problem_id" id="problem_id_hidden">
<label style="font-size: 1.2em; margin-right: 1px; ">提交者:</label>
<div class="field"><input name="submitter" style="width: 100px; " type="text" value="<%= form.submitter %>"></div>
<% if ((typeof contest === 'undefined' || !contest) || contest.ended || contest.type !== 'noi' || (user && user.is_admin)) { %>
<% if ((typeof contest === 'undefined' || !contest) || !((!user || !user.is_admin) && !contest.ended && (contest.type === 'acm' || contest.type === 'noi'))) { %>
<label style="font-size: 1.2em; margin-right: 1px; ">分数:</label>
<div class="field" style="padding-right: 6px; "><input name="min_score" style="width: 45px; " type="text" value="<%= form.min_score || 0 %>"></div>
<label style="font-size: 1.2em; margin-right: 7px; ">~</label>
<div class="field"><input name="max_score" style="width: 45px; " type="text" value="<%= form.max_score || 100 %>"></div>
<% } %>
<label style="font-size: 1.2em; margin-right: 1px; ">语言:</label>
<div class="field">
<div class="ui fluid selection dropdown" id="select_language" style="width: 110px; ">
<input type="hidden" name="language" value="<%= form.language %>">
<i class="dropdown icon"></i>
<div class="default text"></div>
<div class="menu">
<div class="item" data-value="">不限</div>
<% for (let lang in syzoj.config.languages) { %>
<div class="item" data-value="<%= lang %>"><%= syzoj.config.languages[lang].show %></div>
<% } %>
</div>
</div>
</div>
<label style="font-size: 1.2em; margin-right: 1px; ">状态:</label>
<div class="field">
<div class="ui fluid selection dropdown" id="select_status" style="width: 210px; ">
<input type="hidden" name="status" value="<%= form.status %>">
<i class="dropdown icon"></i>
<div class="default text"></div>
<div class="menu">
<div class="item" data-value="">不限<i class="dropdown icon" style="visibility: hidden; "></i></div>
<% for (let status in this.icon) { %>
<% if (this.iconHidden.includes(status)) continue; %>
<div class="item" data-value="<%= status %>"><span class="status <%= status.toLowerCase().split(' ').join('_') %>"><i class="<%= this.icon[status] %> icon"></i> <%= status %></div>
<% } %>
</div>
</div>
</div>
<% } %>
<button class="ui labeled icon mini button" type="submit">
<i class="search icon"></i>
搜索
</button>
<% if (user) { %>
<a class="ui mini labeled icon blue button" style="margin-left: auto; " id="my_submit">
<i class="user icon"></i>
我的提交
</a>
<script>
$(function () {
$('#my_submit').click(function () {
$('[name=submitter]').val(<%- JSON.stringify(user.username) %>);
$('#form').submit();
});
});
</script>
<% } %>
</div>
</form>
<table class="ui very basic center aligned table" style="white-space: nowrap; ">
<thead>
<tr>
<th>编号</th>
<th>题目</th>
<th>状态</th>
<% if ((typeof contest === 'undefined' || !contest) || !((!user || !user.is_admin) && !contest.ended && (contest.type === 'acm' || contest.type === 'noi'))) { %>
<th>分数</th>
<% } %>
<th>总时间</th>
<th>内存</th>
<th>代码</th>
<th>提交者</th>
<th>提交时间</th>
</tr>
</thead>
<tbody>
<% for (let judge of judge_state) { %>
<tr id="submissions_<%= judge.id %>"><% include submissions_item %></tr>
<% } %>
</tbody>
</table>
<br>
<% include page %>
</div>
<script>
$(function () {
$('#select_language').dropdown();
$('#select_status').dropdown();
});
function checkSubmit() {
var x = $('#problem_id').val(), ch = x.charCodeAt(0);
if (x.length === 1 && x >= 'A' && x <= 'Z') $('#problem_id_hidden').val(ch - 'A'.charCodeAt(0) + 1);
else if (x.length === 1 && x >= 'a' && x <= 'a') $('#problem_id_hidden').val(ch - 'a'.charCodeAt(0) + 1);
else $('#problem_id_hidden').val(x);
return true;
}
</script>
<% include footer %>

3
views/submission_content.ejs

@ -1,6 +1,5 @@
<% include util %> <% include util %>
<div class="padding" id="vueApp"> <div class="padding" id="vueApp">
<table class="ui very basic center aligned table" id="status_table"> <table class="ui very basic center aligned table" id="status_table">
<thead> <thead>
@ -152,7 +151,7 @@ const vueApp = new Vue({
running: false running: false
}, },
code: <%- JSON.stringify(code) %>, code: <%- JSON.stringify(code) %>,
detailResult: <%- JSON.stringify(detailResult) %> detailResult: <%- JSON.stringify(detailResult) %>,
}, },
computed: { computed: {
singleSubtask() { singleSubtask() {

37
views/submissions.ejs

@ -3,16 +3,31 @@
<% include util %> <% include util %>
<script src="/textFit.js"></script> <script src="/textFit.js"></script>
<div class="padding"> <div class="padding">
<% if (displayConfig.inContest) { %>
<div class="ui large info message">
<div class="ui header">比赛 - <%= contest.title %></div>
<% if (displayConfig.hideOthers) { %>
<p>您只能看到自己的提交。</p>
<% } else { %>
<p>您可以看到其他人的提交。
<% } %>
</div>
<% } %>
<form action="<%= syzoj.utils.makeUrl(['submissions']) %>" class="ui mini form" method="get" role="form" id="form"> <form action="<%= syzoj.utils.makeUrl(['submissions']) %>" class="ui mini form" method="get" role="form" id="form">
<div class="inline fields" style="margin-bottom: 25px; white-space: nowrap; "> <div class="inline fields" style="margin-bottom: 25px; white-space: nowrap; ">
<label style="font-size: 1.2em; margin-right: 1px; ">题目:</label> <label style="font-size: 1.2em; margin-right: 1px; ">题目:</label>
<div class="field"><input name="problem_id" style="width: 50px; " type="text" value="<%= form.problem_id %>"></div> <div class="field"><input name="problem_id" style="width: 50px; " type="text" value="<%= form.problem_id %>"></div>
<% if (!displayConfig.hideOthers) { %>
<label style="font-size: 1.2em; margin-right: 1px; ">提交者:</label> <label style="font-size: 1.2em; margin-right: 1px; ">提交者:</label>
<div class="field"><input name="submitter" style="width: 100px; " type="text" value="<%= form.submitter %>"></div> <div class="field"><input name="submitter" style="width: 100px; " type="text" value="<%= form.submitter %>"></div>
<% } %>
<% if (!displayConfig.hideScore) { %>
<label style="font-size: 1.2em; margin-right: 1px; ">分数:</label> <label style="font-size: 1.2em; margin-right: 1px; ">分数:</label>
<div class="field" style="padding-right: 6px; "><input name="min_score" style="width: 45px; " type="text" value="<%= form.min_score || 0 %>"></div> <div class="field" style="padding-right: 6px; "><input name="min_score" style="width: 45px; " type="text" value="<%= form.min_score || 0 %>"></div>
<label style="font-size: 1.2em; margin-right: 7px; ">~</label> <label style="font-size: 1.2em; margin-right: 7px; ">~</label>
<div class="field"><input name="max_score" style="width: 45px; " type="text" value="<%= form.max_score || 100 %>"></div> <div class="field"><input name="max_score" style="width: 45px; " type="text" value="<%= form.max_score || 100 %>"></div>
<% } %>
<label style="font-size: 1.2em; margin-right: 1px; ">语言:</label> <label style="font-size: 1.2em; margin-right: 1px; ">语言:</label>
<div class="field"> <div class="field">
<div class="ui fluid selection dropdown" id="select_language" style="width: 110px; "> <div class="ui fluid selection dropdown" id="select_language" style="width: 110px; ">
@ -28,6 +43,7 @@
</div> </div>
</div> </div>
</div> </div>
<% if (!displayConfig.hideResult) { %>
<label style="font-size: 1.2em; margin-right: 1px; ">状态:</label> <label style="font-size: 1.2em; margin-right: 1px; ">状态:</label>
<div class="field"> <div class="field">
<div class="ui fluid selection dropdown" id="select_status" style="width: 210px; "> <div class="ui fluid selection dropdown" id="select_status" style="width: 210px; ">
@ -43,11 +59,12 @@
</div> </div>
</div> </div>
</div> </div>
<% } %>
<button class="ui labeled icon mini button" type="submit"> <button class="ui labeled icon mini button" type="submit">
<i class="search icon"></i> <i class="search icon"></i>
搜索 查询
</button> </button>
<% if (user) { %> <% if (user && !displayConfig.hideOthers) { %>
<a class="ui mini labeled icon blue button" style="margin-left: auto; " id="my_submit"> <a class="ui mini labeled icon blue button" style="margin-left: auto; " id="my_submit">
<i class="user icon"></i> <i class="user icon"></i>
我的提交 我的提交
@ -69,16 +86,16 @@
<th>编号</th> <th>编号</th>
<th>题目</th> <th>题目</th>
<th>状态</th> <th>状态</th>
<th>分数</th> <th v-if="!displayConfig.hideScore">分数</th>
<th>总时间</th> <th v-if="!displayConfig.hideUsage">总时间</th>
<th>内存</th> <th v-if="!displayConfig.hideUsage">内存</th>
<th>代码 / 答案文件</th> <th v-if="!displayConfig.hideCode">代码 / 答案文件</th>
<th>提交者</th> <th>提交者</th>
<th>提交时间</th> <th>提交时间</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="item in items" :data="item" is='submission-item'> <tr v-for="item in items" :config="displayConfig" :data="item" is='submission-item'>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -96,12 +113,14 @@ $(function () {
$('#select_status').dropdown(); $('#select_status').dropdown();
}); });
const itemList = <%- JSON.stringify(items) %>; const itemList = <%- JSON.stringify(items) %>;
const socketUrl = <%- syzoj.utils.judgeServer(displayConfig.pushType) %>;
const displayConfig = <%- JSON.stringify(displayConfig) %>;
const socketUrl = <%- syzoj.utils.judgeServer('rough') %>;
const vueApp = new Vue({ const vueApp = new Vue({
el: '#vueApp', el: '#vueApp',
data: { data: {
items: itemList items: itemList,
displayConfig: displayConfig
}, },
}); });

38
views/submissions_item.ejs

@ -2,19 +2,28 @@
<% include status_label %> <% include status_label %>
<script> <script>
const submissionUrl = <%- JSON.stringify(syzoj.utils.makeUrl(['submission', 'VanDarkholme'])) %>; const submissionUrl = <%- JSON.stringify(displayConfig.inContest ?
const problemUrl = <%- JSON.stringify(syzoj.utils.makeUrl(['problem', 'VanDarkholme'])) %>; syzoj.utils.makeUrl(['contest', contest.id, 'submission', 'VanDarkholme']) :
syzoj.utils.makeUrl(['submission', 'VanDarkholme'])) %>;
const problemUrl = <%- JSON.stringify(displayConfig.inContest ?
syzoj.utils.makeUrl(['contest', contest.id, 'problem', 'VanDarkholme']) :
syzoj.utils.makeUrl(['problem', 'VanDarkholme'])) %>;
Vue.component('submission-item', { Vue.component('submission-item', {
template: '#submissionItemTemplate', template: '#submissionItemTemplate',
props: ['data'], props: ['data', 'config'],
computed: { computed: {
statusString() { statusString() {
const data = this.data; const data = this.data;
if (data.result) { if (data.result) {
return data.result.result; return data.result.result;
} else if (data.running) {
if (this.config.hideResult) {
return 'Compiling';
} else {
return 'Running';
} }
else if (data.running) return 'Running'; } else return 'Waiting';
else return 'Waiting';
}, },
submissionLink() { submissionLink() {
return submissionUrl.replace('VanDarkholme', this.data.info.taskId); return submissionUrl.replace('VanDarkholme', this.data.info.taskId);
@ -25,6 +34,11 @@ Vue.component('submission-item', {
scoreClass() { scoreClass() {
return "score_" + (parseInt(this.data.result.score / 10) || 0).toString(); return "score_" + (parseInt(this.data.result.score / 10) || 0).toString();
} }
},
methods: {
alpha(number) {
if (number && parseInt(number) == number && parseInt(number) > 0) return String.fromCharCode('A'.charCodeAt(0) + parseInt(number) - 1);
}
} }
}); });
</script> </script>
@ -32,18 +46,18 @@ Vue.component('submission-item', {
<script id="submissionItemTemplate" type="text/x-template"> <script id="submissionItemTemplate" type="text/x-template">
<tr> <tr>
<td><a :href="submissionLink">#{{ data.info.taskId }}</a></td> <td><a :href="submissionLink">#{{ data.info.taskId }}</a></td>
<td><a :href="problemLink">#{{ data.info.problemId }}. {{ data.info.problemName }}</a></td> <td><a :href="problemLink">#{{ config.inContest ? alpha(data.info.problemId) : data.info.problemId }}. {{ data.info.problemName }}</a></td>
<td><status-label :status="statusString"></status-label></td> <td><a :href="submissionLink"><status-label :status="statusString"></status-label></a></td>
<template v-if="data.result"> <template v-if="data.result">
<td><span class="score" :class="scoreClass">{{ (data.result.score != null && data.result.score !== NaN) ? Math.floor(data.result.score) : '' }}</span></td> <td v-if="!config.hideScore"><span class="score" :class="scoreClass">{{ (data.result.score != null && data.result.score !== NaN) ? Math.floor(data.result.score) : '' }}</span></td>
<td>{{ (data.result.time != null && data.result.time !== NaN) ? data.result.time.toString() + ' ms' : '' }}</td> <td v-if="!config.hideUsage">{{ (data.result.time != null && data.result.time !== NaN) ? data.result.time.toString() + ' ms' : '' }}</td>
<td>{{ (data.result.memory != null && data.result.memory !== NaN) ? data.result.memory.toString() + ' KiB' : '' }}</td> <td v-if="!config.hideUsage">{{ (data.result.memory != null && data.result.memory !== NaN) ? data.result.memory.toString() + ' KiB' : '' }}</td>
</template> <template v-else> </template> <template v-else>
<td /> <td /> <td /> <td v-if="!config.hideScore"/> <td v-if="!config.hideUsage"/> <td v-if="!config.hideUsage"/>
</template> </template>
<td>{{ data.info.language != null ? data.info.language + ' / ' : '' }}{{ data.info.codeSize }}</td> <td v-if="!config.hideCode">{{ data.info.language != null ? data.info.language + ' / ' : '' }}{{ data.info.codeSize }}</td>
<td>{{ data.info.user }}</td> <td>{{ data.info.user }}</td>
<td>{{ data.info.submitTime }}</td> <td>{{ data.info.submitTime }}</td>
</tr> </tr>

Loading…
Cancel
Save