diff --git a/packages/hydrojudge/package.json b/packages/hydrojudge/package.json index d7ff2a131..f825dedd4 100644 --- a/packages/hydrojudge/package.json +++ b/packages/hydrojudge/package.json @@ -1,7 +1,7 @@ { "name": "@hydrooj/hydrojudge", "bin": "bin/hydrojudge.js", - "version": "2.11.10", + "version": "2.12.0", "main": "package.json", "author": "undefined ", "repository": "https://github.com/hydro-dev/Hydro.git", diff --git a/packages/hydrojudge/src/hosts/hydro.ts b/packages/hydrojudge/src/hosts/hydro.ts index 24ee7a153..fd7df4ccd 100644 --- a/packages/hydrojudge/src/hosts/hydro.ts +++ b/packages/hydrojudge/src/hosts/hydro.ts @@ -28,8 +28,7 @@ class JudgeTask { host: string; request: any; ws: WebSocket; - domainId: string; - pid: string; + source: string; rid: string; lang: string; code: string; @@ -55,20 +54,19 @@ class JudgeTask { this.next = this.next.bind(this); this.end = this.end.bind(this); this.stat.handle = new Date(); - this.domainId = this.request.pdomain; - this.pid = this.request.pid.toString(); this.rid = this.request.rid; this.lang = this.request.lang; this.code = this.request.code; this.config = this.request.config; this.input = this.request.input; this.data = this.request.data; + this.source = this.request.source; this.tmpdir = path.resolve(getConfig('tmp_dir'), this.host, this.rid); this.clean = []; - await Lock.aquire(`${this.host}/${this.domainId}/${this.rid}`); + await Lock.aquire(`${this.host}/${this.source}/${this.rid}`); fs.ensureDirSync(this.tmpdir); tmpfs.mount(this.tmpdir, '512m'); - log.info('Submission: %s/%s/%s pid=%s', this.host, this.domainId, this.rid, this.pid); + log.info('Submission: %s/%s/%s pid=%s', this.host, this.source, this.rid); try { if (typeof this.input === 'string') await this.run(); else await this.doSubmission(); @@ -92,7 +90,7 @@ class JudgeTask { }); } } finally { - Lock.release(`${this.host}/${this.domainId}/${this.rid}`); + Lock.release(`${this.host}/${this.source}/${this.rid}`); for (const clean of this.clean) await clean().catch(noop); tmpfs.umount(this.tmpdir); fs.removeSync(this.tmpdir); @@ -106,12 +104,12 @@ class JudgeTask { async doSubmission() { this.stat.cache_start = new Date(); - this.folder = await this.session.cacheOpen(this.domainId, this.pid, this.data, this.next); + this.folder = await this.session.cacheOpen(this.source, this.data, this.next); this.stat.read_cases = new Date(); this.config = await readCases( this.folder, { detail: this.session.config.detail, ...this.config }, - { next: this.next, key: md5(`${this.domainId}${this.pid}${getConfig('secret')}`) }, + { next: this.next, key: md5(`${this.source}${getConfig('secret')}`) }, ); this.stat.judge = new Date(); const type = this.config.type || 'default'; @@ -175,18 +173,18 @@ export default class Hydro { setInterval(() => { this.axios.get('judge/noop'); }, 30000000); } - async cacheOpen(domainId: string, pid: string, files: any[], next?) { - await Lock.aquire(`${this.config.host}/${domainId}/${pid}`); + async cacheOpen(source: string, files: any[], next?) { + await Lock.aquire(`${this.config.host}/${source}`); try { - return this._cacheOpen(domainId, pid, files, next); + return this._cacheOpen(source, files, next); } finally { - Lock.release(`${this.config.host}/${domainId}/${pid}`); + Lock.release(`${this.config.host}/${source}`); } } - async _cacheOpen(domainId: string, pid: string, files: any[], next?) { - const domainDir = path.join(getConfig('cache_dir'), this.config.host, domainId); - const filePath = path.join(domainDir, pid); + async _cacheOpen(source: string, files: any[], next?) { + const [domainId, pid] = source.split('/'); + const filePath = path.join(getConfig('cache_dir'), this.config.host, source); await fs.ensureDir(filePath); if (!files?.length) throw new SystemError('Problem data not found.'); let etags: Record = {}; @@ -205,11 +203,11 @@ export default class Hydro { if (!allFiles.has(name) && fs.existsSync(path.join(filePath, name))) await fs.rm(path.join(filePath, name)); } if (filenames.length) { - log.info(`Getting problem data: ${this.config.host}/${domainId}/${pid}`); + log.info(`Getting problem data: ${this.config.host}/${source}`); if (next) next({ judge_text: '正在同步测试数据,请稍候' }); await this.ensureLogin(); const res = await this.axios.post(`/d/${domainId}/judge/files`, { - pid, + pid: +pid, files: filenames, }); // eslint-disable-next-line no-inner-declarations diff --git a/packages/hydrojudge/src/service.ts b/packages/hydrojudge/src/service.ts index 627ab2548..1e05258c7 100644 --- a/packages/hydrojudge/src/service.ts +++ b/packages/hydrojudge/src/service.ts @@ -73,8 +73,8 @@ async function postInit() { } } - async function cacheOpen(domainId: string, pid: string, files: any[]) { - const filePath = path.join(getConfig('cache_dir'), domainId, pid); + async function cacheOpen(source: string, files: any[]) { + const filePath = path.join(getConfig('cache_dir'), source); await fs.ensureDir(filePath); if (!files?.length) throw new SystemError('Problem data not found.'); let etags: Record = {}; @@ -87,7 +87,7 @@ async function postInit() { filenames.add(file.name); version[file.name] = file.etag + file.lastModified; if (etags[file.name] !== file.etag + file.lastModified) { - await storage.get(`problem/${domainId}/${pid}/testdata/${file.name}`, path.join(filePath, file.name)); + await storage.get(`problem/${source}/testdata/${file.name}`, path.join(filePath, file.name)); } } for (const name in etags) { @@ -142,9 +142,8 @@ async function postInit() { stat: any; request: any; event: string; - pid: string; + source: string; rid: string; - domainId: string; lang: string; code: string; data: any[]; @@ -168,12 +167,11 @@ async function postInit() { async handle() { try { this.stat.handle = new Date(); - this.pid = (this.request.pid || 'unknown').toString(); this.rid = this.request.rid.toString(); - this.domainId = this.request.pdomain; this.lang = this.request.lang; this.code = this.request.code; this.data = this.request.data; + this.source = this.request.source; this.config = this.request.config; this.input = this.request.input; this.next = getNext(this); @@ -182,7 +180,7 @@ async function postInit() { this.clean = []; fs.ensureDirSync(this.tmpdir); tmpfs.mount(this.tmpdir, getConfig('tmpfs_size')); - logger.info(`Submission: ${this.domainId}/${this.rid}`, { pid: this.pid }); + logger.info(`Submission: ${this.source}/${this.rid}`); if (typeof this.input === 'string') await this.run(); else await this.submission(); } catch (e) { @@ -212,12 +210,12 @@ async function postInit() { async submission() { this.stat.cache_start = new Date(); - this.folder = await cacheOpen(this.domainId, this.pid, this.data); + this.folder = await cacheOpen(this.source, this.data); this.stat.read_cases = new Date(); this.config = await readCases( this.folder, this.config, - { next: this.next, key: md5(`${this.domainId}${this.pid}${getConfig('secret')}`) }, + { next: this.next, key: md5(`${this.source}/${getConfig('secret')}`) }, ); this.stat.judge = new Date(); const type = this.config.type || 'default'; diff --git a/packages/hydrooj/TODO b/packages/hydrooj/TODO deleted file mode 100644 index 934016f77..000000000 --- a/packages/hydrooj/TODO +++ /dev/null @@ -1,12 +0,0 @@ -@requireCsrfToken -举报功能 -SETTINGS_PRIVACY.allowRegisteredUsers -pdoc.secretConfig? -message.markAsRead -检查多余的索引 -+hack -讨论浏览数量显示 -extend problem settings -添加nav排序功能 -自动拓扑组件加载顺序? -add declarations to psdoc \ No newline at end of file diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 55557a7a0..09766f3a3 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "2.39.33", + "version": "3.0.0", "bin": "bin/hydrooj.js", "main": "src/loader", "module": "src/loader", diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 3b6561b12..9ee1f5693 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -10,7 +10,7 @@ import { ProblemNotFoundError, RecordNotFoundError, ValidationError, } from '../error'; import { - DomainDoc, ProblemDoc, Tdoc, User, + ProblemDoc, Tdoc, User, } from '../interface'; import paginate from '../lib/paginate'; import { PERM, PRIV } from '../model/builtin'; @@ -153,10 +153,10 @@ export class ContestScoreboardDownloadHandler extends Handler { await this.limitRate('scoreboard_download', 120, 3); const getContent = { csv: async (rows) => `\uFEFF${rows.map((c) => (c.map((i) => i.value).join(','))).join('\n')}`, - html: (rows) => this.renderHTML('contest_scoreboard_download_html.html', { rows }), + html: (rows, tdoc) => this.renderHTML('contest_scoreboard_download_html.html', { rows, tdoc }), }; const [tdoc, rows] = await contest.getScoreboard.call(this, domainId, tid, true, 0); - this.binary(await getContent[ext](rows), `${tdoc.title}.${ext}`); + this.binary(await getContent[ext](rows, tdoc), `${tdoc.title}.${ext}`); } } @@ -217,13 +217,7 @@ export class ContestEditHandler extends Handler { domainId: string, tid: ObjectID, beginAtDate: string, beginAtTime: string, duration: number, title: string, content: string, rule: string, _pids: string, rated = false, _code = '', ) { - const pids = _pids.replace(/,/g, ',').split(',').map((i) => { - i = i.trim(); - if ((+i).toString() === i) return +i; - if (i.split(':')[0] === domainId) return +i.split(':')[1]; - if (!i.includes(':')) throw new ValidationError('pids'); - return i; - }).filter((i) => i); + const pids = _pids.replace(/,/g, ',').split(',').map((i) => +i).filter((i) => i); const beginAtMoment = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone); if (!beginAtMoment.isValid()) throw new ValidationError('beginAtDate', 'beginAtTime'); const endAt = beginAtMoment.clone().add(duration, 'hours').toDate(); @@ -308,8 +302,10 @@ export class ContestProblemFileDownloadHandler extends ContestProblemHandler { @param('filename', Types.Name) @param('noDisposition', Types.Boolean) async get(domainId: string, filename: string, noDisposition = false) { - // @ts-ignore - if (typeof this.pdoc.docId === 'string') this.pdoc.docId = this.pdoc.docId.split(':')[1]; + if (this.pdoc.reference) { + this.pdoc = await problem.get(this.pdoc.reference.domainId, this.pdoc.reference.pid); + if (!this.pdoc) throw new ProblemNotFoundError(this.pdoc.reference.domainId, this.pdoc.reference.pid); + } const target = `problem/${this.pdoc.domainId}/${this.pdoc.docId}/additional_file/${filename}`; const file = await storage.getMeta(target); if (!file) { @@ -334,25 +330,16 @@ export class ContestProblemFileDownloadHandler extends ContestProblemHandler { } export class ContestDetailProblemSubmitHandler extends ContestProblemHandler { - pdomainId: string; - ppid: number; - pdomain: DomainDoc; - @param('pid', Types.Name) - async prepare(domainId: string, _pid: string) { - const pid = this.tdoc.pids[parseInt(_pid, 36) - 10]; + async prepare() { if (!this.attended) throw new ContestNotAttendedError(this.tdoc.docId); if (!contest.isOngoing(this.tdoc)) throw new ContestNotLiveError(this.tdoc.docId); - this.pdomainId = typeof pid === 'string' ? pid.split(':')[0] : domainId; - this.ppid = typeof pid === 'number' ? pid : +pid.split(':')[1]; - this.pdomain = await domain.get(this.pdomainId); - if (this.pdomain.langs) { + if (this.domain.langs) { this.response.body.pdoc.config.langs = intersection( - this.response.body.pdoc.config.langs || this.pdomain.langs.split(','), - this.pdomain.langs.split(','), + this.response.body.pdoc.config.langs || this.domain.langs.split(','), + this.domain.langs.split(','), ); } - this.response.body.pdoc.config.domainId = this.pdomainId; } @param('tid', Types.ObjectID) @@ -387,7 +374,7 @@ export class ContestDetailProblemSubmitHandler extends ContestProblemHandler { if (!pretest) { await Promise.all([ (this.tsdoc.journal || []).filter((i) => i.pid === pid).length - && problem.inc(this.pdomainId, this.ppid, 'nSubmit', 1), + && problem.inc(this.domainId, this.pdoc.docId, 'nSubmit', 1), domain.incUserInDomain(domainId, this.user._id, 'nSubmit'), contest.updateStatus(domainId, tid, this.user._id, rid, pid), ]); diff --git a/packages/hydrooj/src/handler/homework.ts b/packages/hydrooj/src/handler/homework.ts index 08200cbf3..639e424ea 100644 --- a/packages/hydrooj/src/handler/homework.ts +++ b/packages/hydrooj/src/handler/homework.ts @@ -10,7 +10,7 @@ import { HomeworkNotLiveError, ProblemNotFoundError, ValidationError, } from '../error'; import { - DomainDoc, PenaltyRules, ProblemDoc, ProblemId, Tdoc, User, + PenaltyRules, ProblemDoc, Tdoc, User, } from '../interface'; import paginate from '../lib/paginate'; import { PERM, PRIV, STATUS } from '../model/builtin'; @@ -114,7 +114,7 @@ class HomeworkDetailHandler extends Handler { class HomeworkDetailProblemHandler extends Handler { tdoc: Tdoc<30>; pdoc: ProblemDoc; - pid: ProblemId; + pid: number; tsdoc: any; udoc: User; attended: boolean; @@ -173,8 +173,10 @@ export class HomeworkProblemFileDownloadHandler extends HomeworkDetailProblemHan @param('noDisposition', Types.Boolean) // @ts-ignore async get(domainId: string, filename: string, noDisposition = false) { - // @ts-ignore - if (typeof this.pdoc.docId === 'string') this.pdoc.docId = this.pdoc.docId.split(':')[1]; + if (this.pdoc.reference) { + this.pdoc = await problem.get(this.pdoc.reference.domainId, this.pdoc.reference.pid); + if (!this.pdoc) throw new ProblemNotFoundError(this.pdoc.reference.domainId, this.pdoc.reference.pid); + } const target = `problem/${this.pdoc.domainId}/${this.pdoc.docId}/additional_file/${filename}`; const file = await storage.getMeta(target); if (!file) { @@ -199,24 +201,16 @@ export class HomeworkProblemFileDownloadHandler extends HomeworkDetailProblemHan } class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler { - pdomainId: string; - ppid: number; - pdomain: DomainDoc; - @param('tid', Types.ObjectID) async prepare(domainId: string, tid: ObjectID) { if (!this.attended) throw new HomeworkNotAttendedError(tid); if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid); - this.pdomainId = typeof this.pid === 'string' ? this.pid.split(':')[0] : domainId; - this.ppid = typeof this.pid === 'number' ? this.pid : +this.pid.split(':')[1]; - this.pdomain = await domain.get(this.pdomainId); - if (this.pdomain.langs) { + if (this.domain.langs) { this.response.body.pdoc.config.langs = intersection( - this.response.body.pdoc.config.langs || this.pdomain.langs.split(','), - this.pdomain.langs.split(','), + this.response.body.pdoc.config.langs || this.domain.langs.split(','), + this.domain.langs.split(','), ); } - this.response.body.pdoc.config.domainId = this.pdomainId; } @param('tid', Types.ObjectID) @@ -247,7 +241,7 @@ class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler { if (!pretest) { await Promise.all([ (this.tsdoc.journal || []).filter((i) => i.pid === this.pid).length - && problem.inc(this.pdomainId, this.ppid, 'nSubmit', 1), + && problem.inc(this.domainId, this.pdoc.docId, 'nSubmit', 1), domain.incUserInDomain(domainId, this.user._id, 'nSubmit'), contest.updateStatus(domainId, tid, this.user._id, rid, this.pid, STATUS.STATUS_WAITING), ]); @@ -308,13 +302,7 @@ class HomeworkEditHandler extends Handler { penaltySinceDate: string, penaltySinceTime: string, extensionDays: number, penaltyRules: PenaltyRules, title: string, content: string, _pids: string, rated = false, ) { - const pids = _pids.replace(/,/g, ',').split(',').map((i) => { - i = i.trim(); - if ((+i).toString() === i) return +i; - if (i.split(':')[0] === domainId) return +i.split(':')[1]; - if (!i.includes(':')) throw new ValidationError('pids'); - return i; - }).filter((i) => i); + const pids = _pids.replace(/,/g, ',').split(',').map((i) => +i).filter((i) => i); const tdoc = tid ? await contest.get(domainId, tid) : null; if (!tid) this.checkPerm(PERM.PERM_CREATE_HOMEWORK); else if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_HOMEWORK); diff --git a/packages/hydrooj/src/handler/judge.ts b/packages/hydrooj/src/handler/judge.ts index 76e4cd306..7a5359a25 100644 --- a/packages/hydrooj/src/handler/judge.ts +++ b/packages/hydrooj/src/handler/judge.ts @@ -23,17 +23,16 @@ const logger = new Logger('judge'); export async function postJudge(rdoc: RecordDoc) { if (typeof rdoc.input === 'string') return; const accept = rdoc.status === builtin.STATUS.STATUS_ACCEPTED; - const updated = await problem.updateStatus(rdoc.pdomain, rdoc.pid, rdoc.uid, rdoc._id, rdoc.status, rdoc.score); + const updated = await problem.updateStatus(rdoc.domainId, rdoc.pid, rdoc.uid, rdoc._id, rdoc.status, rdoc.score); if (rdoc.contest) { await contest.updateStatus( rdoc.domainId, rdoc.contest, rdoc.uid, rdoc._id, - rdoc.domainId === rdoc.pdomain ? rdoc.pid : `${rdoc.pdomain}:${rdoc.pid}`, - rdoc.status, rdoc.score, + rdoc.pid, rdoc.status, rdoc.score, ); } else if (accept && updated) await domain.incUserInDomain(rdoc.domainId, rdoc.uid, 'nAccept', 1); const pdoc = (accept && updated) - ? await problem.inc(rdoc.pdomain, rdoc.pid, 'nAccept', 1) - : await problem.get(rdoc.pdomain, rdoc.pid); + ? await problem.inc(rdoc.domainId, rdoc.pid, 'nAccept', 1) + : await problem.get(rdoc.domainId, rdoc.pid); if (pdoc) { const difficulty = difficultyAlgorithm(pdoc.nSubmit, pdoc.nAccept); await Promise.all([ diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index f37e5e4a5..56e1759ee 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -265,6 +265,15 @@ export class ProblemDetailHandler extends ProblemHandler { this.back(); } + @param('target', Types.String) + @param('pid', Types.String, true) + async postCopy(domainId: string, target: string, pid: string) { + const dudoc = await user.getById(target, this.user._id); + dudoc.checkPerm(PERM.PERM_CREATE_PROBLEM); + const docId = await problem.copy(domainId, this.user.docId, target, pid); + this.response.redirect = this.url('problem_detail', { domainId: target, pid: docId }); + } + async postDelete() { if (!this.user.own(this.pdoc, PERM.PERM_EDIT_PROBLEM_SELF)) this.checkPerm(PERM.PERM_EDIT_PROBLEM); const tdocs = await contest.getRelated(this.domainId, this.pdoc.docId); @@ -372,6 +381,7 @@ export class ProblemFilesHandler extends ProblemDetailHandler { @param('pjax', Types.Boolean) async get(domainId: string, getTestdata = true, getAdditionalFile = true, pjax = false) { this.response.body.testdata = getTestdata ? sortFiles(this.pdoc.data || []) : []; + this.response.body.reference = getTestdata ? this.pdoc.reference : ''; this.response.body.additional_file = getAdditionalFile ? sortFiles(this.pdoc.additional_file || []) : []; if (pjax) { const { testdata, additional_file } = this.response.body; @@ -407,6 +417,7 @@ export class ProblemFilesHandler extends ProblemDetailHandler { @post('filename', Types.Name, true) @post('type', Types.Range(['testdata', 'additional_file']), true) async postUploadFile(domainId: string, filename: string, type = 'testdata') { + if (this.pdoc.reference) throw new ForbiddenError('Cannot delete files of a referenced problem.'); if (!this.request.files.file) throw new ValidationError('file'); if (!filename) filename = this.request.files.file.name || String.random(16); if (filename.includes('/') || filename.includes('..')) throw new ValidationError('filename', null, 'Bad filename'); @@ -463,6 +474,7 @@ export class ProblemFilesHandler extends ProblemDetailHandler { @post('files', Types.Array) @post('type', Types.Range(['testdata', 'additional_file']), true) async postDeleteFiles(domainId: string, files: string[], type = 'testdata') { + if (this.pdoc.reference) throw new ForbiddenError('Cannot delete files of a referenced problem.'); if (!this.user.own(this.pdoc, PERM.PERM_EDIT_PROBLEM_SELF)) this.checkPerm(PERM.PERM_EDIT_PROBLEM); if (type === 'testdata') await problem.delTestdata(domainId, this.pdoc.docId, files); else await problem.delAdditionalFile(domainId, this.pdoc.docId, files); @@ -475,6 +487,11 @@ export class ProblemFileDownloadHandler extends ProblemDetailHandler { @param('filename', Types.Name) @param('noDisposition', Types.Boolean) async get(domainId: string, type = 'additional_file', filename: string, noDisposition = false) { + if (this.pdoc.reference) { + if (type === 'testdata') throw new ForbiddenError('Cannot download testdata'); + this.pdoc = await problem.get(this.pdoc.reference.domainId, this.pdoc.reference.pid); + if (!this.pdoc) throw new ProblemNotFoundError(this.pdoc.reference.domainId, this.pdoc.reference.pid); + } if (type === 'testdata' && !this.user.own(this.pdoc)) { if (!this.user.hasPriv(PRIV.PRIV_READ_PROBLEM_DATA)) this.checkPerm(PERM.PERM_READ_PROBLEM_DATA); } @@ -644,18 +661,6 @@ export class ProblemCreateHandler extends Handler { export class ProblemPrefixListHandler extends Handler { @param('prefix', Types.Name) async get(domainId: string, prefix: string) { - let cors = false; - if (prefix.includes(':')) { - const [pdomain, s] = prefix.split(':'); - const ddoc = await domain.get(pdomain); - this.response.body = []; - if (!ddoc || ddoc._id === domainId || !ddoc.share || !s.trim()) return; - ddoc.share = ddoc.share.replace(/,/g, ',').split(',').map((q) => q.trim()).join(','); - if (ddoc.share !== '*' && !`,${ddoc.share},`.includes(`,${domainId},`)) return; - domainId = pdomain; - prefix = s; - cors = true; - } const pdocs = await problem.getPrefixList(domainId, prefix); if (!Number.isNaN(+prefix)) { const pdoc = await problem.get(domainId, +prefix, ['domainId', 'docId', 'pid', 'title']); @@ -670,7 +675,6 @@ export class ProblemPrefixListHandler extends Handler { } } this.response.body = pdocs; - if (cors) this.response.body.forEach((v) => { v.docId = `${domainId}:${v.docId}`; }); } } diff --git a/packages/hydrooj/src/handler/record.ts b/packages/hydrooj/src/handler/record.ts index eafc6df08..69fb5b41e 100644 --- a/packages/hydrooj/src/handler/record.ts +++ b/packages/hydrooj/src/handler/record.ts @@ -76,9 +76,7 @@ class RecordListHandler extends Handler { : await Promise.all([ user.getList(domainId, rdocs.map((rdoc) => rdoc.uid)), canViewProblem - ? problem.getList(domainId, rdocs.map( - (rdoc) => (rdoc.domainId === domainId ? rdoc.pid : `${rdoc.pdomain}:${rdoc.pid}`), - ), canViewProblemHidden || this.user._id, false) + ? problem.getList(domainId, rdocs.map((rdoc) => rdoc.pid), canViewProblemHidden || this.user._id, false) : Object.fromEntries([rdocs.map((rdoc) => [rdoc.pid, { ...problem.default, pid: rdoc.pid }])]), ]); this.response.body = { @@ -125,7 +123,7 @@ class RecordDetailHandler extends Handler { } // eslint-disable-next-line prefer-const let [pdoc, udoc] = await Promise.all([ - problem.get(rdoc.pdomain, rdoc.pid), + problem.get(rdoc.domainId, rdoc.pid), user.getById(domainId, rdoc.uid), ]); if ( @@ -181,7 +179,6 @@ class RecordMainConnectionHandler extends ConnectionHandler { all = false; tid: string; uid: number; - pdomain: string; pid: number; status: number; pretest = false; @@ -216,13 +213,9 @@ class RecordMainConnectionHandler extends ConnectionHandler { } } if (pid) { - const pdomain = pid.includes(':') ? pid.split(':')[0] : domainId; - const ppid = pid.includes(':') ? +pid.split(':')[1] : pid; - const pdoc = await problem.get(pdomain, ppid); - if (pdoc) { - this.pdomain = pdoc.domainId; - this.pid = pdoc.docId; - } else throw new ProblemNotFoundError(domainId, pid); + const pdoc = await problem.get(domainId, pid); + if (pdoc) this.pid = pdoc.docId; + else throw new ProblemNotFoundError(domainId, pid); } if (status) this.status = status; if (all) { @@ -251,7 +244,7 @@ class RecordMainConnectionHandler extends ConnectionHandler { // eslint-disable-next-line prefer-const let [udoc, pdoc] = await Promise.all([ user.getById(this.domainId, rdoc.uid), - problem.get(rdoc.pdomain, rdoc.pid), + problem.get(rdoc.domainId, rdoc.pid), ]); const tdoc = this.tid ? this.tdoc || await contest.get(rdoc.domainId, new ObjectID(this.tid)) : null; if (pdoc && !rdoc.contest) { @@ -290,7 +283,7 @@ class RecordDetailConnectionHandler extends ConnectionHandler { rdoc.compilerTexts = []; } } - const pdoc = await problem.get(rdoc.pdomain, rdoc.pid); + const pdoc = await problem.get(rdoc.domainId, rdoc.pid); let canView = pdoc && this.user.own(pdoc); canView ||= !pdoc?.hidden && this.user.hasPerm(PERM.PERM_VIEW_PROBLEM); canView ||= this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN); diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 5d4267448..39c6abb95 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -226,25 +226,27 @@ export interface Document { declare module './model/problem' { // eslint-disable-next-line @typescript-eslint/no-shadow interface ProblemDoc { - docType: document['TYPE_PROBLEM'], - docId: number, - pid: string, - title: string, - content: string, - nSubmit: number, - nAccept: number, - tag: string[], - data: FileInfo[], - additional_file: FileInfo[], - hidden: boolean, - html?: boolean, - stats?: any, - difficulty?: number, - /** @deprecated */ - category?: string[], + docType: document['TYPE_PROBLEM']; + docId: number; + pid: string; + title: string; + content: string; + nSubmit: number; + nAccept: number; + tag: string[]; + data: FileInfo[]; + additional_file: FileInfo[]; + hidden: boolean; + html?: boolean; + stats?: any; + difficulty?: number; + reference?: { + domainId: string; + pid: number; + }; /** string (errormsg) */ - config: string | ProblemConfig, + config: string | ProblemConfig; } } export type { ProblemDoc } from './model/problem'; @@ -280,7 +282,6 @@ export interface TestCase { export interface RecordDoc { _id: ObjectID; domainId: string; - pdomain: string; pid: number; uid: number; lang: string; @@ -292,6 +293,7 @@ export interface RecordDoc { compilerTexts: string[]; testCases: TestCase[]; rejudged: boolean; + source?: string; /** judge uid */ judger: number; judgeAt: Date; @@ -321,9 +323,6 @@ export interface TrainingNode { pids: number[], } -export type ExternalProblemId = string; // ${string}:${number} -export type ProblemId = ExternalProblemId | number; - export interface Tdoc extends Document { docId: ObjectID, docType: docType & number, @@ -333,7 +332,7 @@ export interface Tdoc true, showRecord: (tdoc, now) => now > tdoc.endAt, stat: (tdoc, journal: AcmJournal[]) => { - const naccept: Record = {}; + const naccept: Record = {}; const effective: Record = {}; const detail: AcmDetail[] = []; let accept = 0; @@ -222,14 +222,7 @@ const oi: ContestRule = { // eslint-disable-next-line @typescript-eslint/no-use-before-define if (isDone(tdoc)) { const psdocs = await Promise.all( - tdoc.pids.map((pid) => { - let pdomain = tdoc.domainId; - if (typeof pid === 'string') { - pdomain = pid.split(':')[0]; - pid = +pid.split(':')[1]; - } - return problem.getMultiStatus(pdomain, { docId: pid, uid: { $in: uids } }).toArray(); - }), + tdoc.pids.map((pid) => problem.getMultiStatus(tdoc.domainId, { docId: pid, uid: { $in: uids } }).toArray()), ); for (const tpsdoc of psdocs) { for (const psdoc of tpsdoc) { @@ -250,9 +243,7 @@ const oi: ContestRule = { { type: 'string', value: tsdoc.score || 0 }, ); for (const pid of tdoc.pids) { - const index = pid.toString().includes(':') - ? `${tsdoc.uid}/${pid.toString().replace(':', '/')}` - : `${tsdoc.uid}/${tdoc.domainId}/${pid}`; + const index = `${tsdoc.uid}/${tdoc.domainId}/${pid}`; // eslint-disable-next-line @typescript-eslint/no-use-before-define const node: ScoreboardNode = (!isExport && isDone(tdoc) && tsddict[pid]?.rid?.toHexString() !== psdict[index]?.rid?.toHexString()) ? { @@ -450,7 +441,7 @@ function _getStatusJournal(tsdoc) { export async function add( domainId: string, title: string, content: string, owner: number, - rule: string, beginAt = new Date(), endAt = new Date(), pids: ProblemId[] = [], + rule: string, beginAt = new Date(), endAt = new Date(), pids: number[] = [], rated = false, data: Partial = {}, ) { if (!this.RULES[rule]) throw new ValidationError('rule'); @@ -489,7 +480,7 @@ export async function get(domainId: string, tid: ObjectID): Promise> { return tdoc; } -export async function getRelated(domainId: string, pid: ProblemId) { +export async function getRelated(domainId: string, pid: number) { return await document.getMulti(domainId, document.TYPE_CONTEST, { pids: pid }).toArray(); } @@ -498,7 +489,7 @@ export function getStatus(domainId: string, tid: ObjectID, uid: number) { } export async function updateStatus( - domainId: string, tid: ObjectID, uid: number, rid: ObjectID, pid: ProblemId, + domainId: string, tid: ObjectID, uid: number, rid: ObjectID, pid: number, status = STATUS.STATUS_WRONG_ANSWER, score = 0, ) { const [tdoc, otsdoc] = await Promise.all([ diff --git a/packages/hydrooj/src/model/problem.ts b/packages/hydrooj/src/model/problem.ts index 719d426ef..f496fa4cd 100644 --- a/packages/hydrooj/src/model/problem.ts +++ b/packages/hydrooj/src/model/problem.ts @@ -1,13 +1,12 @@ import { - escapeRegExp, flatten, groupBy, pick, + escapeRegExp, pick, } from 'lodash'; import { FilterQuery, ObjectID } from 'mongodb'; import type { Readable } from 'stream'; import { streamToBuffer } from '@hydrooj/utils/lib/utils'; import { ProblemNotFoundError, ValidationError } from '../error'; import type { - Document, DomainDoc, ProblemDict, - ProblemId, ProblemStatusDoc, + Document, ProblemDict, ProblemStatusDoc, } from '../interface'; import { parseConfig } from '../lib/testdataConfig'; import * as bus from '../service/bus'; @@ -17,7 +16,6 @@ import { import { buildProjection } from '../utils'; import { STATUS } from './builtin'; import * as document from './document'; -import domain from './domain'; import storage from './storage'; export interface ProblemDoc extends Document { } @@ -33,6 +31,7 @@ export class ProblemModel { static PROJECTION_PUBLIC: Field[] = [ ...ProblemModel.PROJECTION_LIST, 'content', 'html', 'data', 'config', 'additional_file', + 'reference', ]; static default = { @@ -112,12 +111,6 @@ export class ProblemModel { if (typeof pid !== 'number') { if (Number.isSafeInteger(parseInt(pid, 10))) pid = parseInt(pid, 10); } - if (typeof pid === 'string') { - if (pid.includes(':')) { - domainId = pid.split(':')[0]; - pid = +pid.split(':')[1]; - } - } const res = typeof pid === 'number' ? await document.get(domainId, document.TYPE_PROBLEM, pid, projection) : (await document.getMulti(domainId, document.TYPE_PROBLEM, { pid }).toArray())[0]; @@ -151,6 +144,19 @@ export class ProblemModel { return result; } + static async copy(domainId: string, _id: number, target: string, pid?: string) { + const original = await ProblemModel.get(domainId, _id); + if (!original) throw new ProblemNotFoundError(domainId, _id); + if (pid && await ProblemModel.get(target, pid)) pid = ''; + if (!pid && original.pid && !await ProblemModel.get(target, original.pid)) pid = original.pid; + const docId = await ProblemModel.add( + target, pid, original.title, original.content, + original.owner, original.tag, original.hidden, + ); + await ProblemModel.edit(target, docId, { reference: { domainId, pid: _id } }); + return docId; + } + static push>(domainId: string, _id: number, key: ArrayKeys, value: ProblemDoc[T][0]) { return document.push(domainId, document.TYPE_PROBLEM, _id, key, value); } @@ -159,12 +165,7 @@ export class ProblemModel { return document.deleteSub(domainId, document.TYPE_PROBLEM, pid, key, values); } - static inc(domainId: string, _id: ProblemId, field: NumberKeys | string, n: number): Promise { - if (typeof _id === 'string') { - if (!_id.includes(':')) throw new Error(`model.problem.inc: invalid _id <${_id}>`); - domainId = _id.split(':')[0]; - _id = +_id.split(':')[1]; - } + static inc(domainId: string, _id: number, field: NumberKeys | string, n: number): Promise { return document.inc(domainId, document.TYPE_PROBLEM, _id, field as any, n); } @@ -235,42 +236,14 @@ export class ProblemModel { } static async getList( - domainId: string, pids: ProblemId[], + domainId: string, pids: number[], getHidden: number | boolean = false, doThrow = true, projection = ProblemModel.PROJECTION_PUBLIC, ): Promise { - const parsed = groupBy( - Array.from(new Set(pids)).map((i) => ({ - domainId: (typeof i === 'string' && i.includes(':')) ? i.split(':')[0] : domainId, - pid: (typeof i === 'string' && i.includes(':')) ? +i.split(':')[1] : i, - })), - 'domainId', - ); - const r: Record = {}; + const r: Record = {}; const l: Record = {}; - const ddocs = await Promise.all(Object.keys(parsed).map((i) => domain.get(i))); - const f = ddocs.filter((i) => !( - !i || i._id === domainId - || i.share === '*' - || (`,${(i.share || '').replace(/,/g, ',').split(',').map((q) => q.trim()).join(',')},`).includes(`,${domainId},`) - )) as DomainDoc[]; - if (f.length) { - if (doThrow) throw new ProblemNotFoundError(f[0]._id, parsed[f[0]._id][0].pid); - else { - for (const sf of f) { - for (const pinfo of parsed[sf._id]) { - r[pinfo.pid] = { ...ProblemModel.default, domainId: sf._id, pid: pinfo.pid.toString() }; - } - delete parsed[sf._id]; - } - } - } - const tasks = []; - for (const task in parsed) { - const range = { $in: parsed[task].map((i) => i.pid) }; - const q: any = { $or: [{ docId: range }, { pid: range }] }; - tasks.push(document.getMulti(task, document.TYPE_PROBLEM, q).project(buildProjection(projection)).toArray()); - } - let pdocs = flatten(await Promise.all(tasks)); + const q: any = { $or: [{ docId: { $in: pids } }, { pid: { $in: pids } }] }; + let pdocs = await document.getMulti(domainId, document.TYPE_PROBLEM, q) + .project(buildProjection(projection)).toArray(); if (getHidden !== true) pdocs = pdocs.filter((i) => !i.hidden || i.owner === getHidden); for (const pdoc of pdocs) { try { @@ -279,13 +252,8 @@ export class ProblemModel { } catch (e) { pdoc.config = `Cannot parse: ${e.message}`; } - if (pdoc.domainId === domainId) { - r[pdoc.docId] = pdoc; - if (pdoc.pid) l[pdoc.pid] = pdoc; - } else { - r[`${pdoc.domainId}:${pdoc.docId}`] = pdoc; - if (pdoc.pid) l[`${pdoc.domainId}:${pdoc.pid}`] = pdoc; - } + r[pdoc.docId] = pdoc; + if (pdoc.pid) l[pdoc.pid] = pdoc; } // TODO enhance if (pdocs.length !== pids.length) { diff --git a/packages/hydrooj/src/model/record.ts b/packages/hydrooj/src/model/record.ts index ab8487743..87b49865a 100644 --- a/packages/hydrooj/src/model/record.ts +++ b/packages/hydrooj/src/model/record.ts @@ -8,8 +8,7 @@ import { } from 'mongodb'; import { ProblemNotFoundError } from '../error'; import { - ExternalProblemId, FileInfo, ProblemConfigFile, - RecordDoc, + FileInfo, ProblemConfigFile, RecordDoc, } from '../interface'; import * as bus from '../service/bus'; import db from '../service/db'; @@ -23,8 +22,8 @@ class RecordModel { static coll: Collection = db.collection('record'); static PROJECTION_LIST: (keyof RecordDoc)[] = [ '_id', 'score', 'time', 'memory', 'lang', - 'uid', 'pid', 'rejudged', 'progress', 'pdomain', - 'contest', 'judger', 'judgeAt', 'status', 'domainId', + 'uid', 'pid', 'rejudged', 'progress', 'domainId', + 'contest', 'judger', 'judgeAt', 'status', 'source', ]; static async submissionPriority(uid: number, base: number = 0) { @@ -65,9 +64,15 @@ class RecordModel { const rdoc = await RecordModel.get(domainId, rid); if (!rdoc) return null; let data: FileInfo[] = []; + let source = `${domainId}/${rdoc.pid}`; if (rdoc.pid) { - const pdoc = await problem.get(rdoc.pdomain, rdoc.pid); - if (!pdoc) throw new ProblemNotFoundError(rdoc.pdomain, rdoc.pid); + let pdoc = await problem.get(rdoc.domainId, rdoc.pid); + if (!pdoc) throw new ProblemNotFoundError(rdoc.domainId, rdoc.pid); + if (pdoc.reference) { + pdoc = await problem.get(pdoc.reference.domainId, pdoc.reference.pid); + if (!pdoc) throw new ProblemNotFoundError(rdoc.domainId, rdoc.pid); + } + source = `${pdoc.domainId}/${pdoc.docId}`; data = pdoc.data; if (typeof pdoc.config === 'string') throw new Error(pdoc.config); if (pdoc.config.type === 'remote_judge') { @@ -92,25 +97,20 @@ class RecordModel { domainId, config, data, + source, }); } static async add( - domainId: string, pid: ExternalProblemId | number, uid: number, + domainId: string, pid: number, uid: number, lang: string, code: string, addTask: boolean, tidOrInput?: ObjectID | string, isContest?: boolean, ) { - let pdomain = domainId; - if (typeof pid === 'string') { - pdomain = pid.split(':')[0]; - pid = +pid.split(':')[1]; - } const data: RecordDoc = { status: STATUS.STATUS_WAITING, _id: new ObjectID(), uid, code, lang, - pdomain, pid, domainId, score: 0, @@ -214,11 +214,8 @@ class RecordModel { } // Mark problem as deleted -bus.on('problem/delete', (domainId, docId) => RecordModel.coll.updateMany({ pdomain: domainId, pid: docId }, { $set: { pid: -1 } })); -bus.on('domain/delete', (domainId) => Promise.all([ - RecordModel.coll.deleteMany({ domainId }), - RecordModel.coll.updateMany({ pdomain: domainId }, { $set: { docId: -1 } }), -])); +bus.on('problem/delete', (domainId, docId) => RecordModel.coll.updateMany({ domainId, pid: docId }, { $set: { pid: -1 } })); +bus.on('domain/delete', (domainId) => RecordModel.coll.deleteMany({ domainId })); bus.once('app/started', () => db.ensureIndexes( RecordModel.coll, diff --git a/packages/hydrooj/src/upgrade.ts b/packages/hydrooj/src/upgrade.ts index d90c9fc7f..4fed9a76a 100644 --- a/packages/hydrooj/src/upgrade.ts +++ b/packages/hydrooj/src/upgrade.ts @@ -562,6 +562,71 @@ const scripts: UpgradeScript[] = [ await db.collection('domain.user').updateMany({}, { $unset: { ratingHistory: '' } }); return true; }, + async function _51_52() { + const _FRESH_INSTALL_IGNORE = 1; + const mapping: Record = {}; + const isStringPid = (i: string) => i.toString().includes(':'); + async function getProblem(domainId: string, target: string) { + if (!target.toString().includes(':')) return await problem.get(domainId, target); + const l = `${domainId}/${target}`; + if (mapping[l]) return await problem.get(domainId, mapping[l]); + const [sourceDomain, sourceProblem] = target.split(':'); + const docId = await problem.copy(sourceDomain, +sourceProblem, domainId); + mapping[l] = docId; + return await problem.get(domainId, docId); + } + const cursor = db.collection('document').find({ docType: document.TYPE_CONTEST }); + while (await cursor.hasNext()) { + const doc = await cursor.next(); + const pids = []; + let mark = false; + for (const pid of doc.pids) { + if (pid.toString().includes(':')) { + mark = true; + const pdoc = await getProblem(doc.domainId, pid); + if (pdoc) { + pids.push(pdoc.docId); + await RecordModel.updateMulti( + doc.domainId, + { contest: doc.docId }, + { pid: pdoc.docId }, + {}, + { pdomain: '' }, + ); + } + } else pids.push(pid); + } + if (mark) { + const ctdocs = await document.getMultiStatus( + doc.domainId, document.TYPE_CONTEST, { docId: doc.docId }, + ).toArray(); + for (const ctdoc of ctdocs) { + const cmark = ctdoc.journal?.filter((i) => isStringPid(i.pid)).length + || ctdoc.detail?.filter((i) => isStringPid(i.pid)).length; + if (!cmark) continue; + const $set: any = {}; + if (ctdoc.journal?.filter((i) => isStringPid(i.pid)).length) { + $set.journal = await Promise.all(ctdoc.journal.map(async (i) => { + const pdoc = await getProblem(doc.domainId, i.pid); + if (pdoc) i.pid = pdoc.docId; + return i; + })); + } + if (ctdoc.detail?.filter((i) => isStringPid(i.pid)).length) { + $set.detail = await Promise.all(ctdoc.detail.map(async (i) => { + const pdoc = await getProblem(doc.domainId, i.pid); + if (pdoc) i.pid = pdoc.docId; + return i; + })); + } + await document.setStatus(doc.domainId, doc.docType, doc.docId, ctdoc.uid, $set); + } + await contest.edit(doc.domainId, doc.docId, { pids }); + } + } + await db.collection('record').updateMany({}, { $unset: { pdomain: '' } }); + return true; + }, ]; export default scripts; diff --git a/packages/migrate-vijos/script.ts b/packages/migrate-vijos/script.ts index 6d1a1c108..cff0e295d 100644 --- a/packages/migrate-vijos/script.ts +++ b/packages/migrate-vijos/script.ts @@ -217,7 +217,7 @@ const tasks = { code: doc.code, lang: doc.lang, uid: doc.uid, - pdomain: doc.domain_id, + source: `${doc.domainId}/${pid(doc.pid)}`, pid: pid(doc.pid), domainId: doc.domain_id, judger: doc.judge_uid || 1, diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 5dcc1a1b8..e2e034295 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.25.12", + "version": "4.26.0", "author": "undefined ", "license": "AGPL-3.0", "main": "hydro.js", diff --git a/packages/ui-default/templates/contest_scoreboard_download_html.html b/packages/ui-default/templates/contest_scoreboard_download_html.html index b6a61565e..6c0de9964 100644 --- a/packages/ui-default/templates/contest_scoreboard_download_html.html +++ b/packages/ui-default/templates/contest_scoreboard_download_html.html @@ -24,12 +24,18 @@ background: #F0F0F0; } +

{{ tdoc.title }}

+Exported from {{ model.system.get('server.url') }} {%- for column in rows[0] -%} {%- endfor -%} @@ -39,7 +45,24 @@ {%- for column in row -%} {%- endfor -%} diff --git a/packages/ui-default/templates/partials/problem_files-testdata.html b/packages/ui-default/templates/partials/problem_files-testdata.html index a812da3b8..9a947d3e6 100644 --- a/packages/ui-default/templates/partials/problem_files-testdata.html +++ b/packages/ui-default/templates/partials/problem_files-testdata.html @@ -1,6 +1,10 @@ {% import "components/nothing.html" as nothing with context %}
- {% if testdata.length %} + {% if reference %} +
+

{{ _('This is a copy of another problem.') }}

+
+ {% elif testdata.length %}
- {{ column['value'] }} + {% if column.type == 'problem_detail' %} + {{ column.value }} + {% else %} + {{ column.value }} + {% endif %}
- {{ column['value'] }} + {% if column.type == 'record' and column.raw %} + + {{ column.value|replaceBr|safe }} + + {% elif column.type == 'records' %} + {%- for record in column.raw -%} + {% if loop.index0 %}/{% endif %} + {% if record.raw %} + + {{ record.value|replaceBr|safe }} + + {% else %} + {{ record.value|replaceBr|safe }} + {% endif %} + {%- endfor -%} + {% else %} + {{ column.value|replaceBr|safe }} + {% endif %}
diff --git a/packages/ui-default/templates/partials/problem_sidebar_contest.html b/packages/ui-default/templates/partials/problem_sidebar_contest.html index c175a87a4..360b7b48a 100644 --- a/packages/ui-default/templates/partials/problem_sidebar_contest.html +++ b/packages/ui-default/templates/partials/problem_sidebar_contest.html @@ -24,8 +24,7 @@ {{ _('Submit') }} {% elif model.contest.isDone(tdoc) %} - {% set _docId = pdoc.docId.toString() %} - {% endif %} diff --git a/packages/ui-default/templates/partials/problem_sidebar_homework.html b/packages/ui-default/templates/partials/problem_sidebar_homework.html index 1e9f257fc..390d699ec 100644 --- a/packages/ui-default/templates/partials/problem_sidebar_homework.html +++ b/packages/ui-default/templates/partials/problem_sidebar_homework.html @@ -24,8 +24,7 @@ {{ _('Submit') }} {% elif model.contest.isDone(tdoc) %} - {% set _docId = pdoc.docId.toString() %} - {% endif %} diff --git a/packages/ui-default/templates/problem_detail.html b/packages/ui-default/templates/problem_detail.html index 508546410..3bd7e29e3 100644 --- a/packages/ui-default/templates/problem_detail.html +++ b/packages/ui-default/templates/problem_detail.html @@ -76,7 +76,7 @@

{{ pdoc.title }}

- {% if not pdoc.data.length %} + {% if not pdoc.data.length and not pdoc.reference %}

{{ _('No testdata at current.') }}

diff --git a/packages/ui-default/templates/problem_files.html b/packages/ui-default/templates/problem_files.html index 0e15832e9..927144131 100644 --- a/packages/ui-default/templates/problem_files.html +++ b/packages/ui-default/templates/problem_files.html @@ -6,7 +6,7 @@

{{ _('Testdata') }}

- {% if handler.user.own(pdoc, perm.PERM_EDIT_PROBLEM_SELF) or handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) %} + {% if not reference and (handler.user.own(pdoc, perm.PERM_EDIT_PROBLEM_SELF) or handler.user.hasPerm(perm.PERM_EDIT_PROBLEM)) %} {% endif %} @@ -14,7 +14,7 @@
{{ noscript_note.render() }} {% include "partials/problem_files-testdata.html" %} - {% if handler.user.own(pdoc) or handler.user.hasPerm(perm.PERM_READ_PROBLEM_DATA) or handler.user.hasPriv(PRIV.PRIV_READ_PROBLEM_DATA) %} + {% if not reference and (handler.user.own(pdoc) or handler.user.hasPerm(perm.PERM_READ_PROBLEM_DATA) or handler.user.hasPriv(PRIV.PRIV_READ_PROBLEM_DATA)) %}
{% if handler.user.own(pdoc, perm.PERM_EDIT_PROBLEM_SELF) or handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) %} diff --git a/packages/ui-default/templates/record_main.html b/packages/ui-default/templates/record_main.html index 683006fa2..b9f777cce 100644 --- a/packages/ui-default/templates/record_main.html +++ b/packages/ui-default/templates/record_main.html @@ -92,7 +92,7 @@
{%- for rdoc in rdocs -%} {% set udoc = udict[rdoc['uid']] %} - {% set pdoc = pdict[rdoc['pid']] or pdict[rdoc.pdomain + ':' + rdoc.pid] %} + {% set pdoc = pdict[rdoc['pid']] %} {% include 'record_main_tr.html' %} {%- endfor -%}