From c417e1e6604077b1ea4c1fe76a31e01f492e1e2c Mon Sep 17 00:00:00 2001 From: Menci Date: Sat, 24 Jun 2017 23:39:34 +0800 Subject: [PATCH] Add testdata view and manage --- config-example.json | 1 + models/problem.js | 114 +++++++++++++++- models/user.js | 2 - modules/api.js | 8 -- modules/contest.js | 2 + modules/problem.js | 76 +++++++++-- modules/user.js | 1 - utility.js | 150 ++++++++++++--------- views/problem.ejs | 10 +- views/problem_data.ejs | 262 ++++++++++++------------------------ views/problem_manage.ejs | 144 ++++++++++++++++++++ views/problem_testcases.ejs | 42 ++++++ 12 files changed, 538 insertions(+), 274 deletions(-) create mode 100644 views/problem_manage.ejs create mode 100644 views/problem_testcases.ejs diff --git a/config-example.json b/config-example.json index a6158ae..b39106d 100644 --- a/config-example.json +++ b/config-example.json @@ -29,6 +29,7 @@ "time_limit": 10000, "memory_limit": 1024, "data_size": 209715200, + "testdata": 209715200, "submit_code": 102400, "submit_answer": 10485760 }, diff --git a/models/problem.js b/models/problem.js index bb8f68b..dbd74b2 100644 --- a/models/problem.js +++ b/models/problem.js @@ -232,7 +232,6 @@ let model = db.define('problem', { time_limit: { type: Sequelize.INTEGER }, memory_limit: { type: Sequelize.INTEGER }, - testdata_id: { type: Sequelize.INTEGER }, additional_file_id: { type: Sequelize.INTEGER }, ac_num: { type: Sequelize.INTEGER }, @@ -286,14 +285,13 @@ class Problem extends Model { file_io_input_name: '', file_io_output_name: '', - type: '' + type: 'traditional' }, val))); } async loadRelationships() { this.user = await User.fromID(this.user_id); this.publicizer = await User.fromID(this.publicizer_id); - this.testdata = await File.fromID(this.testdata_id); this.additional_file = await File.fromID(this.additional_file_id); } @@ -316,12 +314,102 @@ class Problem extends Model { return user.is_admin; } + getTestdataPath() { + return syzoj.utils.resolvePath(syzoj.config.upload_dir, 'testdata', this.id.toString()); + } + + async updateTestdata(path) { + let AdmZip = require('adm-zip'); + let zip = new AdmZip(path); + + let unzipSize = 0; + for (let x of zip.getEntries()) unzipSize += x.header.size; + if (unzipSize > syzoj.config.limit.testdata) throw new ErrorMessage('数据包太大。'); + + let dir = this.getTestdataPath(); + let fs = Promise.promisifyAll(require('fs-extra')); + await fs.removeAsync(dir); + await fs.ensureDirAsync(dir); + + zip.extractAllTo(dir); + await fs.moveAsync(path, dir + '.zip', { overwrite: true }); + } + + async uploadTestdataSingleFile(filename, filepath, size) { + let dir = this.getTestdataPath(); + let fs = Promise.promisifyAll(require('fs-extra')), path = require('path'); + await fs.ensureDirAsync(dir); + + let oldSize = 0; + let list = await this.listTestdata(); + if (list) { + for (let file of list.files) if (file.filename !== filename) oldSize += file.size; + } + + if (oldSize + size > syzoj.config.limit.testdata) throw new ErrorMessage('数据包太大。'); + + await fs.moveAsync(filepath, path.join(dir, filename), { overwrite: true }); + + let AdmZip = require('adm-zip'); + let zip = new AdmZip(); + + list = await this.listTestdata(); + for (let file of list.files) zip.addLocalFile(path.join(dir, file.filename), '', file.filename); + zip.writeZip(dir + '.zip'); + } + + async hasSpecialJudge() { + try { + let fs = Promise.promisifyAll(require('fs-extra')); + let dir = this.getTestdataPath(); + let list = await fs.readdirAsync(dir); + return list.includes('spj.js') || list.find(x => x.startsWith('spj_')) !== undefined; + } catch (e) { + return false; + } + } + + async listTestdata() { + try { + let fs = Promise.promisifyAll(require('fs-extra')), path = require('path'); + let dir = this.getTestdataPath(); + let list = await fs.readdirAsync(dir); + list = await list.mapAsync(async x => { + let stat = await fs.statAsync(path.join(dir, x)); + if (!stat.isFile()) return undefined; + return { + filename: x, + size: stat.size + } + }); + + list = list.filter(x => x); + + let res = { + files: list, + zip: null + }; + + try { + let stat = await fs.statAsync(this.getTestdataPath() + '.zip'); + if (stat.isFile()) { + res.zip = { + size: stat.size + } + } + } catch (e) {} + + return res; + } catch (e) { + console.log(e); + return null; + } + } + async updateFile(path, type) { let file = await File.upload(path, type); - if (type === 'testdata') { - this.testdata_id = file.id; - } else if (type === 'additional_file') { + if (type === 'additional_file') { this.additional_file_id = file.id; } @@ -505,7 +593,21 @@ class Problem extends Model { } } + let oldTestdataDir = this.getTestdataPath(), oldTestdataZip = oldTestdataDir + '.zip'; + this.id = id; + + // Move testdata + let newTestdataDir = this.getTestdataPath(), newTestdataZip = newTestdataDir + '.zip'; + let fs = Promise.promisifyAll(require('fs-extra')); + if (await syzoj.utils.isDir(oldTestdataDir)) { + await fs.moveAsync(oldTestdataDir, newTestdataDir); + } + + if (await syzoj.utils.isFile(oldTestdataZip)) { + await fs.moveAsync(oldTestdataZip, newTestdataZip); + } + await this.save(); } diff --git a/models/user.js b/models/user.js index 8d9926a..5ddc670 100644 --- a/models/user.js +++ b/models/user.js @@ -197,8 +197,6 @@ class User extends Model { let oldPrivileges = await this.getPrivileges(); - console.log(newPrivileges); - let delPrivileges = oldPrivileges.filter(x => !newPrivileges.includes(x)); let addPrivileges = newPrivileges.filter(x => !oldPrivileges.includes(x)); diff --git a/modules/api.js b/modules/api.js index 4bf61e7..b7cc274 100644 --- a/modules/api.js +++ b/modules/api.js @@ -229,14 +229,6 @@ app.post('/api/update_judge/:id', async (req, res) => { } }); -app.get('/static/uploads/testdata/:md5', async (req, res) => { - try { - res.sendFile(File.resolvePath('testdata', req.params.md5)); - } catch (e) { - res.status(500).send(e); - } -}); - app.get('/static/uploads/answer/:md5', async (req, res) => { try { res.sendFile(File.resolvePath('answer', req.params.md5)); diff --git a/modules/contest.js b/modules/contest.js index 35969fd..e7240c6 100644 --- a/modules/contest.js +++ b/modules/contest.js @@ -334,6 +334,8 @@ app.get('/contest/:id/:pid', async (req, res) => { let problem_id = problems_id[pid - 1]; let problem = await Problem.fromID(problem_id); + problem.specialJudge = await problem.hasSpecialJudge(); + await syzoj.utils.markdown(problem, [ 'description', 'input_format', 'output_format', 'example', 'limit_and_hint' ]); let state = await problem.getJudgeState(res.locals.user, false); diff --git a/modules/problem.js b/modules/problem.js index 099fe6c..50bdf26 100644 --- a/modules/problem.js +++ b/modules/problem.js @@ -190,6 +190,7 @@ app.get('/problem/:id', async (req, res) => { problem.allowedEdit = await problem.isAllowedEditBy(res.locals.user); problem.allowedManage = await problem.isAllowedManageBy(res.locals.user); + problem.specialJudge = await problem.hasSpecialJudge(); if (problem.is_public || problem.allowedEdit) { await syzoj.utils.markdown(problem, [ 'description', 'input_format', 'output_format', 'example', 'limit_and_hint' ]); @@ -427,7 +428,7 @@ app.post('/problem/:id/import', async (req, res) => { let fs = require('bluebird').promisifyAll(require('fs')); try { - let data = await download(req.body.url + (req.body.url.endsWith('/') ? 'download' : '/download')); + let data = await download(req.body.url + (req.body.url.endsWith('/') ? 'testdata/download' : '/testdata/download')); await fs.writeFileAsync(tmpFile.path, data); await problem.updateTestdata(tmpFile.path); } catch (e) { @@ -443,7 +444,8 @@ app.post('/problem/:id/import', async (req, res) => { } }); -app.get('/problem/:id/data', async (req, res) => { +// The 'manage' is not `allow manage`'s 'manage', I just have no better name for it. +app.get('/problem/:id/manage', async (req, res) => { try { let id = parseInt(req.params.id); let problem = await Problem.fromID(id); @@ -453,8 +455,11 @@ app.get('/problem/:id/data', async (req, res) => { await problem.loadRelationships(); - res.render('problem_data', { - problem: problem + let testcases = await syzoj.utils.parseTestdata(problem.getTestdataPath()); + + res.render('problem_manage', { + problem: problem, + testcases: testcases }); } catch (e) { syzoj.log(e); @@ -464,7 +469,7 @@ app.get('/problem/:id/data', async (req, res) => { } }); -app.post('/problem/:id/data', app.multer.fields([{ name: 'testdata', maxCount: 1 }, { name: 'additional_file', maxCount: 1 }]), async (req, res) => { +app.post('/problem/:id/manage', app.multer.fields([{ name: 'testdata', maxCount: 1 }, { name: 'additional_file', maxCount: 1 }]), async (req, res) => { try { let id = parseInt(req.params.id); let problem = await Problem.fromID(id); @@ -491,7 +496,7 @@ app.post('/problem/:id/data', app.multer.fields([{ name: 'testdata', maxCount: 1 if (validateMsg) throw new ErrorMessage('无效的题目数据配置。', null, validateMsg); if (req.files['testdata']) { - await problem.updateFile(req.files['testdata'][0].path, 'testdata'); + await problem.updateTestdata(req.files['testdata'][0].path); } if (req.files['additional_file']) { @@ -500,7 +505,7 @@ app.post('/problem/:id/data', app.multer.fields([{ name: 'testdata', maxCount: 1 await problem.save(); - res.redirect(syzoj.utils.makeUrl(['problem', id, 'data'])); + res.redirect(syzoj.utils.makeUrl(['problem', id, 'manage'])); } catch (e) { syzoj.log(e); res.render('error', { @@ -621,7 +626,7 @@ app.post('/problem/:id/submit', app.multer.fields([{ name: 'answer', maxCount: 1 } }); -app.get('/problem/:id/download/testdata', async (req, res) => { +app.get('/problem/:id/testdata', async (req, res) => { try { let id = parseInt(req.params.id); let problem = await Problem.fromID(id); @@ -629,11 +634,60 @@ app.get('/problem/:id/download/testdata', async (req, res) => { if (!problem) throw new ErrorMessage('无此题目。'); if (!await problem.isAllowedUseBy(res.locals.user)) throw new ErrorMessage('您没有权限进行此操作。'); - await problem.loadRelationships(); + let testdata = await problem.listTestdata(); + let testcases = await syzoj.utils.parseTestdata(problem.getTestdataPath()); + + problem.allowedEdit = await problem.isAllowedEditBy(res.locals.user) + + res.render('problem_data', { + problem: problem, + testdata: testdata, + testcases: testcases + }); + } catch (e) { + syzoj.log(e); + res.status(404); + res.render('error', { + err: e + }); + } +}); + +app.post('/problem/:id/testdata/upload', app.multer.array('file'), async (req, res) => { + try { + let id = parseInt(req.params.id); + let problem = await Problem.fromID(id); + + if (!problem) throw new ErrorMessage('无此题目。'); + if (!await problem.isAllowedEditBy(res.locals.user)) throw new ErrorMessage('您没有权限进行此操作。'); - if (!problem.testdata) throw new ErrorMessage('无测试数据。'); + if (req.files) { + for (let file of req.files) { + await problem.uploadTestdataSingleFile(file.originalname, file.path, file.size); + } + } + + res.redirect(syzoj.utils.makeUrl(['problem', id, 'testdata'])); + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }); + } +}); + +app.get('/problem/:id/testdata/download/:filename?', async (req, res) => { + try { + let id = parseInt(req.params.id); + let problem = await Problem.fromID(id); + + if (!problem) throw new ErrorMessage('无此题目。'); + if (!await problem.isAllowedUseBy(res.locals.user)) throw new ErrorMessage('您没有权限进行此操作。'); - res.download(problem.testdata.getPath(), `testdata_${id}.zip`); + let path = require('path'); + let filename = req.params.filename ? path.join(problem.getTestdataPath(), req.params.filename) : (problem.getTestdataPath() + '.zip'); + if (!await syzoj.utils.isFile(filename)) throw new ErrorMessage('文件不存在。'); + res.download(filename, path.basename(filename)); } catch (e) { syzoj.log(e); res.status(404); diff --git a/modules/user.js b/modules/user.js index de5739e..508bc94 100644 --- a/modules/user.js +++ b/modules/user.js @@ -179,7 +179,6 @@ app.post('/user/:id/edit', async (req, res) => { error_info: '' }); } catch (e) { - console.log(e); user.privileges = await user.getPrivileges(); res.locals.user.allowedManage = await res.locals.user.hasPrivilege('manage_user'); diff --git a/utility.js b/utility.js index a271be5..6e24d16 100644 --- a/utility.js +++ b/utility.js @@ -36,7 +36,9 @@ global.ErrorMessage = class ErrorMessage { } }; +let Promise = require('bluebird'); let path = require('path'); +let fs = Promise.promisifyAll(require('fs-extra')); let util = require('util'); let renderer = require('moemark-renderer'); let moment = require('moment'); @@ -44,7 +46,6 @@ let url = require('url'); let querystring = require('querystring'); let pygmentize = require('pygmentize-bundled-cached'); let gravatar = require('gravatar'); -let AdmZip = require('adm-zip'); let filesize = require('file-size'); let AsyncLock = require('async-lock'); @@ -195,82 +196,89 @@ module.exports = { gravatar(email, size) { return gravatar.url(email, { s: size, d: 'mm' }).replace('www', 'cn'); }, - parseTestdata(filename) { - let zip = new AdmZip(filename); - let list = zip.getEntries().filter(e => !e.isDirectory).map(e => e.entryName); - let res = []; - if (!list.includes('data_rule.txt')) { - res[0] = {}; - res[0].cases = []; - for (let file of list) { - let parsedName = path.parse(file); - if (parsedName.ext === '.in') { - if (list.includes(`${parsedName.name}.out`)) { - res[0].cases.push({ - input: file, - output: `${parsedName.name}.out` - }); - } + async parseTestdata(dir) { + if (!await syzoj.utils.isDir(dir)) return null; + + try { + // Get list of *files* + let list = await (await fs.readdirAsync(dir)).filterAsync(async x => await syzoj.utils.isFile(path.join(dir, x))); + + let res = []; + if (!list.includes('data_rule.txt')) { + res[0] = {}; + res[0].cases = []; + for (let file of list) { + let parsedName = path.parse(file); + if (parsedName.ext === '.in') { + if (list.includes(`${parsedName.name}.out`)) { + res[0].cases.push({ + input: file, + output: `${parsedName.name}.out` + }); + } - if (list.includes(`${parsedName.name}.ans`)) { - res[0].cases.push({ - input: file, - output: `${parsedName.name}.ans` - }); + if (list.includes(`${parsedName.name}.ans`)) { + res[0].cases.push({ + input: file, + output: `${parsedName.name}.ans` + }); + } } } - } - res[0].type = 'sum'; - res[0].score = 100; - res[0].cases.sort((a, b) => { - function getLastInteger(s) { - let re = /(\d+)\D*$/; - let x = re.exec(s); - if (x) return parseInt(x[1]); - else return -1; - } + res[0].type = 'sum'; + res[0].score = 100; + res[0].cases.sort((a, b) => { + function getLastInteger(s) { + let re = /(\d+)\D*$/; + let x = re.exec(s); + if (x) return parseInt(x[1]); + else return -1; + } - return getLastInteger(a.input) - getLastInteger(b.input); - }); - } else { - let lines = zip.readAsText('data_rule.txt').split('\r').join('').split('\n').filter(x => x.length !== 0); + return getLastInteger(a.input) - getLastInteger(b.input); + }); + } else { + let lines = (await fs.readFileAsync(path.join(dir, 'data_rule.txt'))).toString().split('\r').join('').split('\n').filter(x => x.length !== 0); - if (lines.length < 3) throw '无效的数据配置文件(data_rule.txt)。'; + if (lines.length < 3) throw '无效的数据配置文件(data_rule.txt)。'; - let input = lines[lines.length - 2]; - let output = lines[lines.length - 1]; + let input = lines[lines.length - 2]; + let output = lines[lines.length - 1]; - for (let s = 0; s < lines.length - 2; ++s) { - res[s] = {}; - res[s].cases = []; - let numbers = lines[s].split(' ').filter(x => x); - if (numbers[0].includes(':')) { - let tokens = numbers[0].split(':'); - res[s].type = tokens[0] || 'sum'; - res[s].score = parseFloat(tokens[1]) || (100 / (lines.length - 2)); - numbers.shift(); - } else { - res[s].type = 'sum'; - res[s].score = 100; - } - for (let i of numbers) { - let testcase = { - input: input.replace('#', i), - output: output.replace('#', i) - }; + for (let s = 0; s < lines.length - 2; ++s) { + res[s] = {}; + res[s].cases = []; + let numbers = lines[s].split(' ').filter(x => x); + if (numbers[0].includes(':')) { + let tokens = numbers[0].split(':'); + res[s].type = tokens[0] || 'sum'; + res[s].score = parseFloat(tokens[1]) || (100 / (lines.length - 2)); + numbers.shift(); + } else { + res[s].type = 'sum'; + res[s].score = 100; + } + for (let i of numbers) { + let testcase = { + input: input.replace('#', i), + output: output.replace('#', i) + }; - if (!list.includes(testcase.input)) throw `找不到文件 ${testcase.input}`; - if (!list.includes(testcase.output)) throw `找不到文件 ${testcase.output}`; - res[s].cases.push(testcase); + if (!list.includes(testcase.input)) throw `找不到文件 ${testcase.input}`; + if (!list.includes(testcase.output)) throw `找不到文件 ${testcase.output}`; + res[s].cases.push(testcase); + } } + + res = res.filter(x => x.cases && x.cases.length !== 0); } - res = res.filter(x => x.cases && x.cases.length !== 0); + res.spj = list.includes('spj.js') || list.some(s => s.startsWith('spj_')); + return res; + } catch (e) { + return { error: e }; } - - res.spj = list.includes('spj.js') || list.some(s => s.startsWith('spj_')); - return res; }, ansiToHTML(s) { let Convert = require('ansi-to-html'); @@ -340,5 +348,19 @@ module.exports = { let crypto = require('crypto'); let decipher = crypto.createDecipher('aes-256-ctr', password); return Buffer.concat([decipher.update(buffer), decipher.final()]); + }, + async isFile(path) { + try { + return (await fs.statAsync(path)).isFile(); + } catch (e) { + return false; + } + }, + async isDir(path) { + try { + return (await fs.statAsync(path)).isDirectory(); + } catch (e) { + return false; + } } }; diff --git a/views/problem.ejs b/views/problem.ejs index d55df79..ef5a84b 100644 --- a/views/problem.ejs +++ b/views/problem.ejs @@ -52,6 +52,10 @@ if (contest) { 标准输入输出 <% } %> +
+ 评测方式:<%= problem.specialJudge ? 'Special Judge' : '文本比较' %> + 题目类型:<%= { 'submit-answer': '答案提交', 'interaction': '交互', 'traditional': '传统' }[problem.type] %> +
上传者: <% if (problem.is_anonymous && !problem.allowedManage) { %> @@ -88,15 +92,15 @@ if (contest) { 提交 提交记录 统计 - <% if (problem.testdata) { %>下载测试数据<% } %> + 测试数据 <% } %> - <% if (problem.additional_file) { %>下载附加文件<% } %> + <% if (problem.additional_file) { %>附加文件<% } %>
<% if (!contest) { %>
<% if (problem.allowedEdit) { %> 编辑 - 管理测试数据 + 管理 <% } %> <% if (problem.allowedManage) { %> <% if (problem.is_public) { %> diff --git a/views/problem_data.ejs b/views/problem_data.ejs index cc1c904..37c9245 100644 --- a/views/problem_data.ejs +++ b/views/problem_data.ejs @@ -1,188 +1,92 @@ -<% this.title = '上传测试数据'; %> -<% include header %> <% -let subtaskType = { - sum: '测试点分数按百分比相加', - min: '取各测试点最低分', - mul: '测试点分数按百分比相乘' -}; +this.title = '测试数据'; +function getIcon(filename) { + let a = { + '.cpp': 'file code outline', + '.c': 'file code outline', + '.cs': 'file code outline', + '.pas': 'file code outline', + '.py': 'file code outline', + '.js': 'file code outline', + '.java': 'file code outline', + '.hs': 'file code outline', + '.vala': 'file code outline', + '.lua': 'file code outline', + '.rb': 'file code outline', + '.vb': 'file code outline', + '.ml': 'file code outline', + '.in': 'file text outline', + '.out': 'file text outline', + '.ans': 'file text outline', + '.txt': 'file text outline', + '.md': 'file text outline', + '.md': 'file text outline', + '.docx': 'file word outline', + '.odt': 'file word outline', + '.xlsx': 'file excel outline', + '.ods': 'file excel outline', + '.pptx': 'file powerpoint outline', + '.odp': 'file powerpoint outline', + '.zip': 'file archive outline', + '.7z': 'file archive outline', + } + for (let x in a) if (filename.endsWith(x)) return a[x]; + return 'file outline'; +} %> +<% include header %>
-
-
-
- <% if (problem.testdata) { %> - <% - try { - let list = syzoj.utils.parseTestdata(problem.testdata.getPath()); - %> - <% if (list.spj) { %> -

评测方式:Special Judge

- <% } else { %> -

评测方式:文本比较

- <% } %> - - - <% let i = 0; %> - <% for (let subtask of list) { %> - <% if (list.length !== 1) { %> - - - <% } else { %> - - - <% } %> - <% for (let testcase of subtask.cases) { %> - - - - - <% } %> - <% } %> - -

子任务 <%= ++i %>

<%= subtaskType[subtask.type] %>,总分值 <%= subtask.score %> -

单个子任务

<%= subtaskType[subtask.type] %> -
<%= testcase.input %><%= testcase.output %>
- <% } catch (e) { %> -

数据包错误:<%= e %>

+
+
+

测试点信息

+ <% include problem_testcases %> +
+
+

文件列表

+ <% if (testdata) { %> + + + + + + + + + + <% if (testdata.zip) { %> + + + + + <% } %> - <% } else { %> -

数据未上传

- <% } %> - -
-
- - -
-
-
- - -
-
- - -
-
- <% if (!problem.file_io) { %> -
- -
-
- - -
-
-
-
- - -
-
-
-
-
- - -
-
- - -
-
- <% } else { %> -
- -
-
- - -
-
-
-
- - -
-
-
-
-
- - -
-
- - + <% if (testdata.files) for (let file of testdata.files) { %> +
+ + + + + <% } %> + +
文件名文件大小操作
完整数据包<%= syzoj.utils.formatSize(testdata.zip.size) %>
<%= file.filename %><%= syzoj.utils.formatSize(file.size) %>
+ <% } else { %> +

无测试数据

+ <% } %> + <% if (problem.allowedEdit) { %> + +
+
+ + +
+ + 返回题目
- <% } %>
-
- 为了避免系统出错,已有提交的题目不允许在提交答案和非提交答案之间更改。
- 提交答案题目不需要设置时间限制、空间限制以及 IO 方式。
- 提交时使用的答案文件名需要与测试数据中每个测试点输出文件相同,且后缀名为 .out。 -
-
- - -
-
- - -
- - 返回题目 -
+ <% } %> +
-
- -<% include footer %> +
+<% include footer %> \ No newline at end of file diff --git a/views/problem_manage.ejs b/views/problem_manage.ejs new file mode 100644 index 0000000..23d419e --- /dev/null +++ b/views/problem_manage.ejs @@ -0,0 +1,144 @@ +<% this.title = '管理题目'; %> +<% include header %> +
+
+
+
+ <% include problem_testcases %> +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ <% if (!problem.file_io) { %> +
+ +
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+ + +
+
+ <% } else { %> +
+ +
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+ + +
+
+ <% } %> +
+
+ 为了避免系统出错,已有提交的题目不允许在提交答案和非提交答案之间更改。
+ 提交答案题目不需要设置时间限制、空间限制以及 IO 方式。
+ 提交时使用的答案文件名需要与测试数据中每个测试点输出文件相同,且后缀名为 .out。 +
+
+ + +
+
+ + +
+ + 返回题目 +
+
+
+
+ +<% include footer %> diff --git a/views/problem_testcases.ejs b/views/problem_testcases.ejs new file mode 100644 index 0000000..f3b0b53 --- /dev/null +++ b/views/problem_testcases.ejs @@ -0,0 +1,42 @@ +<% +let subtaskType = { + sum: '测试点分数按百分比相加', + min: '取各测试点最低分', + mul: '测试点分数按百分比相乘' +}; +%> +<% if (testcases && testcases.error) { %> +

数据包错误:<%= testcases.error %>

+<% +} else if (testcases) { +%> + <% if (testcases.spj) { %> +

评测方式:Special Judge

+ <% } else { %> +

评测方式:文本比较

+ <% } %> + + + <% let i = 0; %> + <% for (let subtask of testcases) { %> + <% if (testcases.length !== 1) { %> + + + <% } else { %> + + + <% } %> + <% for (let testcase of subtask.cases) { %> + + + + + <% } %> + <% } %> + +

子任务 <%= ++i %>

<%= subtaskType[subtask.type] %>,总分值 <%= subtask.score %> +

单个子任务

<%= subtaskType[subtask.type] %> +
<%= testcase.input %><%= testcase.output %>
+<% } else { %> +

无测试数据

+<% } %> \ No newline at end of file