You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
637 lines
18 KiB
637 lines
18 KiB
/* |
|
* 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 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 \ |
|
LIMIT 1 \ |
|
) 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 \ |
|
LIMIT 1 \ |
|
) 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 \ |
|
LIMIT 1 \ |
|
) 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 \ |
|
LIMIT 1 \ |
|
) 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 \ |
|
LIMIT 1 \ |
|
) 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 \ |
|
LIMIT 1 \ |
|
) 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 \ |
|
LIMIT 1 \ |
|
) 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 \ |
|
LIMIT 1 \ |
|
) 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 \ |
|
LIMIT 1 \ |
|
) 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 \ |
|
LIMIT 1 \ |
|
) AS `submit_time` \ |
|
FROM `judge_state` `outer_table` \ |
|
WHERE \ |
|
`problem_id` = __PROBLEM_ID__ AND `status` = "Accepted" AND `type` = 0 \ |
|
ORDER BY `submit_time` ASC \ |
|
', |
|
min: |
|
' \ |
|
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 `max_memory` ASC \ |
|
LIMIT 1 \ |
|
) AS `id`, \ |
|
( \ |
|
SELECT \ |
|
`max_memory` \ |
|
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 `max_memory` ASC \ |
|
LIMIT 1 \ |
|
) AS `max_memory` \ |
|
FROM `judge_state` `outer_table` \ |
|
WHERE \ |
|
`problem_id` = __PROBLEM_ID__ AND `status` = "Accepted" AND `type` = 0 \ |
|
ORDER BY `max_memory` ASC \ |
|
', |
|
max: |
|
' \ |
|
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 `max_memory` ASC \ |
|
LIMIT 1 \ |
|
) AS `id`, \ |
|
( \ |
|
SELECT \ |
|
`max_memory` \ |
|
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 `max_memory` ASC \ |
|
LIMIT 1 \ |
|
) AS `max_memory` \ |
|
FROM `judge_state` `outer_table` \ |
|
WHERE \ |
|
`problem_id` = __PROBLEM_ID__ AND `status` = "Accepted" AND `type` = 0 \ |
|
ORDER BY `max_memory` DESC \ |
|
' |
|
}; |
|
|
|
let Sequelize = require('sequelize'); |
|
let db = syzoj.db; |
|
|
|
let User = syzoj.model('user'); |
|
let File = syzoj.model('file'); |
|
|
|
let model = db.define('problem', { |
|
id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, |
|
|
|
title: { type: Sequelize.STRING(80) }, |
|
user_id: { |
|
type: Sequelize.INTEGER, |
|
references: { |
|
model: 'user', |
|
key: 'id' |
|
} |
|
}, |
|
publicizer_id: { |
|
type: Sequelize.INTEGER, |
|
references: { |
|
model: 'user', |
|
key: 'id' |
|
} |
|
}, |
|
is_anonymous: { type: Sequelize.BOOLEAN }, |
|
|
|
description: { type: Sequelize.TEXT }, |
|
input_format: { type: Sequelize.TEXT }, |
|
output_format: { type: Sequelize.TEXT }, |
|
example: { type: Sequelize.TEXT }, |
|
limit_and_hint: { type: Sequelize.TEXT }, |
|
|
|
time_limit: { type: Sequelize.INTEGER }, |
|
memory_limit: { type: Sequelize.INTEGER }, |
|
|
|
additional_file_id: { type: Sequelize.INTEGER }, |
|
|
|
ac_num: { type: Sequelize.INTEGER }, |
|
submit_num: { type: Sequelize.INTEGER }, |
|
is_public: { type: Sequelize.BOOLEAN }, |
|
|
|
file_io: { type: Sequelize.BOOLEAN }, |
|
file_io_input_name: { type: Sequelize.TEXT }, |
|
file_io_output_name: { type: Sequelize.TEXT }, |
|
|
|
type: { |
|
type: Sequelize.ENUM, |
|
values: ['traditional', 'submit-answer', 'interaction'] |
|
} |
|
}, { |
|
timestamps: false, |
|
tableName: 'problem', |
|
indexes: [ |
|
{ |
|
fields: ['title'], |
|
}, |
|
{ |
|
fields: ['user_id'], |
|
} |
|
] |
|
}); |
|
|
|
let Model = require('./common'); |
|
class Problem extends Model { |
|
static async create(val) { |
|
return Problem.fromRecord(Problem.model.build(Object.assign({ |
|
title: '', |
|
user_id: '', |
|
publicizer_id: '', |
|
is_anonymous: false, |
|
description: '', |
|
|
|
input_format: '', |
|
output_format: '', |
|
example: '', |
|
limit_and_hint: '', |
|
|
|
time_limit: syzoj.config.default.problem.time_limit, |
|
memory_limit: syzoj.config.default.problem.memory_limit, |
|
|
|
ac_num: 0, |
|
submit_num: 0, |
|
is_public: false, |
|
|
|
file_io: false, |
|
file_io_input_name: '', |
|
file_io_output_name: '', |
|
|
|
type: 'traditional' |
|
}, val))); |
|
} |
|
|
|
async loadRelationships() { |
|
this.user = await User.fromID(this.user_id); |
|
this.publicizer = await User.fromID(this.publicizer_id); |
|
this.additional_file = await File.fromID(this.additional_file_id); |
|
} |
|
|
|
async isAllowedEditBy(user) { |
|
if (!user) return false; |
|
if (await user.hasPrivilege('manage_problem')) return true; |
|
return this.user_id === user.id; |
|
} |
|
|
|
async isAllowedUseBy(user) { |
|
if (this.is_public) return true; |
|
if (!user) return false; |
|
if (await user.hasPrivilege('manage_problem')) return true; |
|
return this.user_id === user.id; |
|
} |
|
|
|
async isAllowedManageBy(user) { |
|
if (!user) return false; |
|
if (await user.hasPrivilege('manage_problem')) return true; |
|
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 }); |
|
await fs.removeAsync(dir + '.zip'); |
|
} |
|
|
|
async deleteTestdataSingleFile(filename) { |
|
let dir = this.getTestdataPath(); |
|
let fs = Promise.promisifyAll(require('fs-extra')), path = require('path'); |
|
await fs.removeAsync(path.join(dir, filename)); |
|
await fs.removeAsync(dir + '.zip'); |
|
} |
|
|
|
async makeTestdataZip() { |
|
let dir = this.getTestdataPath(); |
|
if (!await syzoj.utils.isDir(dir)) throw new ErrorMessage('无测试数据。'); |
|
|
|
let AdmZip = require('adm-zip'); |
|
let zip = new AdmZip(); |
|
|
|
let list = await this.listTestdata(); |
|
for (let file of list.files) zip.addLocalFile(require('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) { |
|
if (list) { |
|
res.zip = { |
|
size: null |
|
}; |
|
} |
|
} |
|
|
|
return res; |
|
} catch (e) { |
|
return null; |
|
} |
|
} |
|
|
|
async updateFile(path, type) { |
|
let file = await File.upload(path, type); |
|
|
|
if (type === 'additional_file') { |
|
this.additional_file_id = file.id; |
|
} |
|
|
|
await this.save(); |
|
} |
|
|
|
async validate() { |
|
if (this.time_limit <= 0) return 'Invalid time limit'; |
|
if (this.time_limit > syzoj.config.limit.time_limit) return 'Time limit too large'; |
|
if (this.memory_limit <= 0) return 'Invalid memory limit'; |
|
if (this.memory_limit > syzoj.config.limit.memory_limit) return 'Memory limit too large'; |
|
|
|
let filenameRE = /^[\w \-\+\.]*$/; |
|
if (this.file_io_input_name && !filenameRE.test(this.file_io_input_name)) return 'Invalid input file name'; |
|
if (this.file_io_output_name && !filenameRE.test(this.file_io_output_name)) return 'Invalid output file name'; |
|
|
|
if (this.file_io) { |
|
if (!this.file_io_input_name) return 'No input file name'; |
|
if (!this.file_io_output_name) return 'No output file name'; |
|
} |
|
|
|
return null; |
|
} |
|
|
|
async getJudgeState(user, acFirst) { |
|
if (!user) return null; |
|
|
|
let JudgeState = syzoj.model('judge_state'); |
|
|
|
let where = { |
|
user_id: user.id, |
|
problem_id: this.id |
|
}; |
|
|
|
if (acFirst) { |
|
where.status = 'Accepted'; |
|
|
|
let state = await JudgeState.findOne({ |
|
where: where, |
|
order: [['submit_time', 'desc']] |
|
}); |
|
|
|
if (state) return state; |
|
} |
|
|
|
if (where.status) delete where.status; |
|
|
|
return await JudgeState.findOne({ |
|
where: where, |
|
order: [['submit_time', 'desc']] |
|
}); |
|
} |
|
|
|
// 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; |
|
if (!paginate.pageCnt) a = []; |
|
else 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; |
|
} |
|
|
|
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.color > b.color ? 1 : -1; |
|
}); |
|
|
|
return res; |
|
} |
|
|
|
async setTags(newTagIDs) { |
|
let ProblemTagMap = syzoj.model('problem_tag_map'); |
|
|
|
let oldTagIDs = (await this.getTags()).map(x => x.id); |
|
|
|
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: this.id, |
|
tag_id: tagID |
|
} }); |
|
|
|
await map.destroy(); |
|
} |
|
|
|
for (let tagID of addTagIDs) { |
|
let map = await ProblemTagMap.create({ |
|
problem_id: this.id, |
|
tag_id: tagID |
|
}); |
|
|
|
await map.save(); |
|
} |
|
} |
|
|
|
async changeID(id) { |
|
id = parseInt(id); |
|
await db.query('UPDATE `problem` SET `id` = ' + id + ' WHERE `id` = ' + this.id); |
|
await db.query('UPDATE `judge_state` SET `problem_id` = ' + id + ' WHERE `problem_id` = ' + this.id); |
|
await db.query('UPDATE `problem_tag_map` SET `problem_id` = ' + id + ' WHERE `problem_id` = ' + this.id); |
|
|
|
let Contest = syzoj.model('contest'); |
|
let contests = await Contest.all(); |
|
for (let contest of contests) { |
|
let problemIDs = await contest.getProblems(); |
|
|
|
let flag = false; |
|
for (let i in problemIDs) { |
|
if (problemIDs[i] === this.id) { |
|
problemIDs[i] = id; |
|
flag = true; |
|
} |
|
} |
|
|
|
if (flag) { |
|
await contest.setProblemsNoCheck(problemIDs); |
|
await contest.save(); |
|
} |
|
} |
|
|
|
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(); |
|
} |
|
|
|
getModel() { return model; } |
|
} |
|
|
|
Problem.model = model; |
|
|
|
module.exports = Problem;
|
|
|