core: 删除题目前检查是否已被比赛使用

This commit is contained in:
undefined committed 2021-05-20 22:55:47 +08:00
1 parent e4c6cf258d
commit 26ded9d5df
9 files changed
+74 -58

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "hydrooj", "name": "hydrooj",
"version": "2.26.18", "version": "2.26.19",
"bin": "bin/hydrooj.js", "bin": "bin/hydrooj.js",
"main": "dist/loader.js", "main": "dist/loader.js",
"typings": "dist/loader.d.ts", "typings": "dist/loader.d.ts",
+8 -17
View File
@@ -1,7 +1,6 @@
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import { ObjectID } from 'mongodb'; import { ObjectID } from 'mongodb';
import AdmZip from 'adm-zip'; import AdmZip from 'adm-zip';
import { isSafeInteger } from 'lodash';
import { import {
ContestNotLiveError, ValidationError, ProblemNotFoundError, ContestNotLiveError, ValidationError, ProblemNotFoundError,
ContestNotAttendedError, PermissionError, BadRequestError, ContestNotAttendedError, PermissionError, BadRequestError,
@@ -215,11 +214,8 @@ class ContestEditHandler extends Handler {
domainId: string, beginAtDate: string, beginAtTime: string, duration: number, domainId: string, beginAtDate: string, beginAtTime: string, duration: number,
title: string, content: string, rule: string, _pids: string, rated = false, title: string, content: string, rule: string, _pids: string, rated = false,
) { ) {
const pids = _pids.replace(/,/g, ',').split(',').map((i) => { const pids = _pids.replace(/,/g, ',').split(',').map((i) => +i);
if (isSafeInteger(parseInt(i, 10))) return parseInt(i, 10); await problem.getList(domainId, pids, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN), true);
return i;
});
await problem.getList(domainId, pids, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN), false);
const beginAtMoment = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone); const beginAtMoment = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone);
if (!beginAtMoment.isValid()) { if (!beginAtMoment.isValid()) {
throw new ValidationError('beginAtDate', 'beginAtTime'); throw new ValidationError('beginAtDate', 'beginAtTime');
@@ -389,7 +385,7 @@ class ContestCodeHandler extends Handler {
for (const rdoc of rdocs) { for (const rdoc of rdocs) {
zip.addFile(`${rnames[rdoc._id.toHexString()]}.${rdoc.lang}`, Buffer.from(rdoc.code)); zip.addFile(`${rnames[rdoc._id.toHexString()]}.${rdoc.lang}`, Buffer.from(rdoc.code));
} }
await this.binary(zip.toBuffer(), `${tdoc.title}.zip`); this.binary(zip.toBuffer(), `${tdoc.title}.zip`);
} }
} }
@@ -429,19 +425,14 @@ class ContestCreateHandler extends Handler {
domainId: string, beginAtDate: string, beginAtTime: string, duration: number, domainId: string, beginAtDate: string, beginAtTime: string, duration: number,
title: string, content: string, rule: string, _pids: string, rated = false, title: string, content: string, rule: string, _pids: string, rated = false,
) { ) {
const pids = _pids.replace(/,/g, ',').split(',').map((i) => { const pids = _pids.replace(/,/g, ',').split(',').map((i) => +i);
if (isSafeInteger(parseInt(i, 10))) return parseInt(i, 10);
return i;
});
const beginAt = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone); const beginAt = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone);
if (!beginAt.isValid()) { if (!beginAt.isValid()) throw new ValidationError('beginAtDate', 'beginAtTime');
throw new ValidationError('beginAtDate', 'beginAtTime'); const endAt = beginAt.clone().add(duration, 'hours').toDate();
} await problem.getList(domainId, pids, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN), true);
const endAt = beginAt.clone().add(duration, 'hours');
await problem.getList(domainId, pids, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN), false);
const tid = await contest.add( const tid = await contest.add(
domainId, title, content, domainId, title, content,
this.user._id, rule, beginAt.toDate(), endAt.toDate(), pids, rated, this.user._id, rule, beginAt.toDate(), endAt, pids, rated,
); );
this.response.body = { tid }; this.response.body = { tid };
this.response.redirect = this.url('contest_detail', { tid }); this.response.redirect = this.url('contest_detail', { tid });
@@ -6,7 +6,6 @@ import paginate from '../lib/paginate';
import * as system from '../model/system'; import * as system from '../model/system';
import user from '../model/user'; import user from '../model/user';
import * as oplog from '../model/oplog'; import * as oplog from '../model/oplog';
import message from '../model/message';
import * as discussion from '../model/discussion'; import * as discussion from '../model/discussion';
import * as document from '../model/document'; import * as document from '../model/document';
import { PERM, PRIV } from '../model/builtin'; import { PERM, PRIV } from '../model/builtin';
+38 -31
View File
@@ -2,12 +2,14 @@ import yaml from 'js-yaml';
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import { ObjectID } from 'mongodb'; import { ObjectID } from 'mongodb';
import AdmZip from 'adm-zip'; import AdmZip from 'adm-zip';
import { isSafeInteger } from 'lodash';
import { import {
ValidationError, HomeworkNotLiveError, ProblemNotFoundError, ValidationError, HomeworkNotLiveError, ProblemNotFoundError,
HomeworkNotAttendedError, HomeworkNotAttendedError,
BadRequestError,
} from '../error'; } from '../error';
import { PenaltyRules, Tdoc, ProblemDoc } from '../interface'; import {
PenaltyRules, Tdoc, ProblemDoc, User,
} from '../interface';
import { import {
Route, Handler, Types, param, Route, Handler, Types, param,
} from '../service/server'; } from '../service/server';
@@ -22,6 +24,7 @@ import problem from '../model/problem';
import record from '../model/record'; import record from '../model/record';
import * as document from '../model/document'; import * as document from '../model/document';
import paginate from '../lib/paginate'; import paginate from '../lib/paginate';
import { parseConfig } from '../lib/testdataConfig';
const validatePenaltyRules = (input: string) => { const validatePenaltyRules = (input: string) => {
const s = yaml.load(input); const s = yaml.load(input);
@@ -120,6 +123,8 @@ class HomeworkDetailProblemHandler extends Handler {
tdoc: Tdoc<60>; tdoc: Tdoc<60>;
pdoc: ProblemDoc; pdoc: ProblemDoc;
tsdoc: any; tsdoc: any;
udoc: User;
attended: boolean;
@param('tid', Types.ObjectID) @param('tid', Types.ObjectID)
@param('pid', Types.UnsignedInt) @param('pid', Types.UnsignedInt)
@@ -130,28 +135,38 @@ class HomeworkDetailProblemHandler extends Handler {
problem.get(domainId, pid, this.user._id), problem.get(domainId, pid, this.user._id),
contest.getStatus(domainId, tid, this.user._id, document.TYPE_HOMEWORK), contest.getStatus(domainId, tid, this.user._id, document.TYPE_HOMEWORK),
]); ]);
this.udoc = await user.getById(domainId, this.tdoc.owner);
this.attended = this.tsdoc && this.tsdoc.attend === 1;
this.response.body = {
tdoc: this.tdoc,
pdoc: this.pdoc,
tsdoc: this.tsdoc,
udoc: this.udoc,
attended: this.attended,
};
try {
this.response.body.pdoc.config = await parseConfig(this.pdoc.config);
} catch (e) {
this.response.body.pdoc.config = `Cannot parse: ${e.message}`;
}
} }
@param('tid', Types.ObjectID) @param('tid', Types.ObjectID)
@param('pid', Types.UnsignedInt) @param('pid', Types.UnsignedInt)
async get(domainId: string, tid: ObjectID, pid: number) { async get(domainId: string, tid: ObjectID, pid: number) {
const udoc = await user.getById(domainId, this.tdoc.owner);
const attended = this.tsdoc && this.tsdoc.attend === 1;
if (!contest.isDone(this.tdoc)) { if (!contest.isDone(this.tdoc)) {
if (!attended) throw new HomeworkNotAttendedError(tid); if (!this.attended) throw new HomeworkNotAttendedError(tid);
if (contest.isNotStarted(this.tdoc)) throw new HomeworkNotLiveError(tid); if (contest.isNotStarted(this.tdoc)) throw new HomeworkNotLiveError(tid);
} }
if (!this.tdoc.pids.includes(pid)) throw new ProblemNotFoundError(domainId, pid, tid); if (!this.tdoc.pids.includes(pid)) throw new ProblemNotFoundError(domainId, pid, tid);
const path = [ this.response.template = 'problem_detail.html';
this.response.body.page_name = 'homework_detail_problem';
this.response.body.path = [
['Hydro', 'homepage'], ['Hydro', 'homepage'],
['homework_main', 'homework_main'], ['homework_main', 'homework_main'],
[this.tdoc.title, 'homework_detail', { tid }, true], [this.tdoc.title, 'homework_detail', { tid }, true],
[this.pdoc.title, null, null, true], [this.pdoc.title, null, null, true],
]; ];
this.response.template = 'problem_detail.html';
this.response.body = {
tdoc: this.tdoc, pdoc: this.pdoc, tsdoc: this.tsdoc, udoc, attended, path, page_name: 'homework_detail_problem',
};
} }
} }
@@ -159,16 +174,14 @@ class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler {
@param('tid', Types.ObjectID) @param('tid', Types.ObjectID)
@param('pid', Types.UnsignedInt) @param('pid', Types.UnsignedInt)
async get(domainId: string, tid: ObjectID, pid: number) { async get(domainId: string, tid: ObjectID, pid: number) {
const udoc = await user.getById(domainId, this.tdoc.owner); if (!this.attended) throw new HomeworkNotAttendedError(tid);
const attended = this.tsdoc && this.tsdoc.attend === 1;
if (!attended) throw new HomeworkNotAttendedError(tid);
if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid); if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid);
if (!this.tdoc.pids.includes(pid)) throw new ProblemNotFoundError(domainId, pid, tid); if (!this.tdoc.pids.includes(pid)) throw new ProblemNotFoundError(domainId, pid, tid);
const rdocs = contest.canShowRecord.call(this, this.tdoc) this.response.body.rdocs = contest.canShowRecord.call(this, this.tdoc)
? await record.getUserInProblemMulti(domainId, this.user._id, pid, true) ? await record.getUserInProblemMulti(domainId, this.user._id, pid, true)
.sort('_id', -1).limit(10).toArray() .sort('_id', -1).limit(10).toArray()
: []; : [];
const path = [ this.response.body.path = [
['Hydro', 'homepage'], ['Hydro', 'homepage'],
['homework_main', 'homework_main'], ['homework_main', 'homework_main'],
[this.tdoc.title, 'homework_detail', { tid }, true], [this.tdoc.title, 'homework_detail', { tid }, true],
@@ -176,9 +189,6 @@ class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler {
['homework_detail_problem_submit', null], ['homework_detail_problem_submit', null],
]; ];
this.response.template = 'problem_submit.html'; this.response.template = 'problem_submit.html';
this.response.body = {
tdoc: this.tdoc, pdoc: this.pdoc, tsdoc: this.tsdoc, udoc, attended, path, rdocs,
};
} }
@param('tid', Types.ObjectID) @param('tid', Types.ObjectID)
@@ -186,9 +196,11 @@ class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler {
@param('code', Types.Content) @param('code', Types.Content)
@param('lang', Types.Name) @param('lang', Types.Name)
async post(domainId: string, tid: ObjectID, pid: number, code: string, lang: string) { async post(domainId: string, tid: ObjectID, pid: number, code: string, lang: string) {
if (this.response.body.pdoc.config?.langs && !this.response.body.pdoc.config.langs.includes('lang')) {
throw new BadRequestError('Language not allowed.');
}
await this.limitRate('add_record', 60, 5); await this.limitRate('add_record', 60, 5);
const tsdoc = await contest.getStatus(domainId, tid, this.user._id, document.TYPE_HOMEWORK); if (!this.attended) throw new HomeworkNotAttendedError(tid);
if (!tsdoc.attend) throw new HomeworkNotAttendedError(tid);
if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid); if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid);
if (!this.tdoc.pids.includes(pid)) throw new ProblemNotFoundError(domainId, pid); if (!this.tdoc.pids.includes(pid)) throw new ProblemNotFoundError(domainId, pid);
const rid = await record.add(domainId, pid, this.user._id, lang, code, true, { const rid = await record.add(domainId, pid, this.user._id, lang, code, true, {
@@ -203,7 +215,8 @@ class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler {
rid, pid, false, 0, document.TYPE_HOMEWORK), rid, pid, false, 0, document.TYPE_HOMEWORK),
]); ]);
bus.boardcast('record/change', rdoc); bus.boardcast('record/change', rdoc);
this.response.body = { tid, rid }; this.response.body.tid = tid;
this.response.body.rid = rid;
if (contest.canShowRecord.call(this, this.tdoc)) this.response.redirect = this.url('record_detail', { rid }); if (contest.canShowRecord.call(this, this.tdoc)) this.response.redirect = this.url('record_detail', { rid });
else this.response.redirect = this.url('homework_detail', { tid }); else this.response.redirect = this.url('homework_detail', { tid });
} }
@@ -246,10 +259,7 @@ class HomeworkCreateHandler extends Handler {
penaltySinceDate: string, penaltySinceTime: string, extensionDays: number, penaltySinceDate: string, penaltySinceTime: string, extensionDays: number,
penaltyRules: PenaltyRules, title: string, content: string, _pids: string, rated = false, penaltyRules: PenaltyRules, title: string, content: string, _pids: string, rated = false,
) { ) {
const pids = _pids.replace(/,/g, ',').split(',').map((i) => { const pids = _pids.replace(/,/g, ',').split(',').map((i) => +i);
if (isSafeInteger(parseInt(i, 10))) return parseInt(i, 10);
return i;
});
const beginAt = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone); const beginAt = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone);
if (!beginAt.isValid()) throw new ValidationError('beginAtDate', 'beginAtTime'); if (!beginAt.isValid()) throw new ValidationError('beginAtDate', 'beginAtTime');
const penaltySince = moment.tz(`${penaltySinceDate} ${penaltySinceTime}`, this.user.timeZone); const penaltySince = moment.tz(`${penaltySinceDate} ${penaltySinceTime}`, this.user.timeZone);
@@ -257,7 +267,7 @@ class HomeworkCreateHandler extends Handler {
const endAt = penaltySince.clone().add(extensionDays, 'days'); const endAt = penaltySince.clone().add(extensionDays, 'days');
if (beginAt.isSameOrAfter(penaltySince)) throw new ValidationError('endAtDate', 'endAtTime'); if (beginAt.isSameOrAfter(penaltySince)) throw new ValidationError('endAtDate', 'endAtTime');
if (penaltySince.isAfter(endAt)) throw new ValidationError('extensionDays'); if (penaltySince.isAfter(endAt)) throw new ValidationError('extensionDays');
await problem.getList(domainId, pids, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN), false); await problem.getList(domainId, pids, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN), true);
const tid = await contest.add(domainId, title, content, this.user._id, const tid = await contest.add(domainId, title, content, this.user._id,
'homework', beginAt.toDate(), endAt.toDate(), pids, rated, 'homework', beginAt.toDate(), endAt.toDate(), pids, rated,
{ penaltySince: penaltySince.toDate(), penaltyRules }, document.TYPE_HOMEWORK); { penaltySince: penaltySince.toDate(), penaltyRules }, document.TYPE_HOMEWORK);
@@ -313,10 +323,7 @@ class HomeworkEditHandler extends Handler {
penaltySinceDate: string, penaltySinceTime: string, extensionDays: number, penaltySinceDate: string, penaltySinceTime: string, extensionDays: number,
penaltyRules: string, title: string, content: string, _pids: string, rated = false, penaltyRules: string, title: string, content: string, _pids: string, rated = false,
) { ) {
const pids = _pids.replace(/,/g, ',').split(',').map((i) => { const pids = _pids.replace(/,/g, ',').split(',').map((i) => +i);
if (isSafeInteger(parseInt(i, 10))) return parseInt(i, 10);
return i;
});
const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK); const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK);
if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_HOMEWORK); if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_HOMEWORK);
else this.checkPerm(PERM.PERM_EDIT_HOMEWORK_SELF); else this.checkPerm(PERM.PERM_EDIT_HOMEWORK_SELF);
@@ -334,7 +341,7 @@ class HomeworkEditHandler extends Handler {
} }
let endAt = penaltySince.clone().add(extensionDays, 'days'); let endAt = penaltySince.clone().add(extensionDays, 'days');
if (beginAt.isSameOrAfter(penaltySince)) throw new ValidationError('endAtDate', 'endAtTime'); if (beginAt.isSameOrAfter(penaltySince)) throw new ValidationError('endAtDate', 'endAtTime');
await problem.getList(domainId, pids, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN), false); await problem.getList(domainId, pids, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN), true);
beginAt = beginAt.toDate(); beginAt = beginAt.toDate();
endAt = endAt.toDate(); endAt = endAt.toDate();
penaltySince = penaltySince.toDate(); penaltySince = penaltySince.toDate();
+12 -1
View File
@@ -19,6 +19,8 @@ import problem from '../model/problem';
import record from '../model/record'; import record from '../model/record';
import domain from '../model/domain'; import domain from '../model/domain';
import user from '../model/user'; import user from '../model/user';
import * as document from '../model/document';
import * as contest from '../model/contest';
import solution from '../model/solution'; import solution from '../model/solution';
import { PERM, PRIV } from '../model/builtin'; import { PERM, PRIV } from '../model/builtin';
import storage from '../service/storage'; import storage from '../service/storage';
@@ -213,11 +215,14 @@ export class ProblemDetailHandler extends ProblemHandler {
async get(..._args: any[]) { async get(..._args: any[]) {
// Navigate to current additional file download // Navigate to current additional file download
// e.g. ![img](a.jpg) will navigate to ![img](./pid/file/a.jpg) // e.g. ![img](a.jpg) will navigate to ![img](./pid/file/a.jpg)
if (!this.request.json) {
this.response.body.pdoc.content = this.response.body.pdoc.content this.response.body.pdoc.content = this.response.body.pdoc.content
.replace(/\(file:\/\//g, `(./${this.pdoc.docId}/file/`); .replace(/\(file:\/\//g, `(./${this.pdoc.docId}/file/`);
}
if (this.pdoc.psdoc) { if (this.pdoc.psdoc) {
this.response.body.rdoc = await record.get(this.domainId, this.pdoc.psdoc.rid); this.response.body.rdoc = await record.get(this.domainId, this.pdoc.psdoc.rid);
} }
this.response.body.ctdocs = await contest.getRelated(this.domainId, this.pdoc.docId);
} }
@param('pid', Types.UnsignedInt) @param('pid', Types.UnsignedInt)
@@ -233,6 +238,12 @@ export class ProblemDetailHandler extends ProblemHandler {
async postDelete() { async postDelete() {
if (!this.user.own(this.pdoc, PERM.PERM_EDIT_PROBLEM_SELF)) this.checkPerm(PERM.PERM_EDIT_PROBLEM); if (!this.user.own(this.pdoc, PERM.PERM_EDIT_PROBLEM_SELF)) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
const [ctdocs, htdocs] = await Promise.all([
contest.getRelated(this.domainId, this.pdoc.docId, document.TYPE_CONTEST),
contest.getRelated(this.domainId, this.pdoc.docId, document.TYPE_HOMEWORK),
]);
if (ctdocs.length) throw new BadRequestError('Problem already used by contest {0}', ctdocs[0]._id);
if (htdocs.length) throw new BadRequestError('Problem already used by homwrork {0}', htdocs[0]._id);
await problem.del(this.pdoc.domainId, this.pdoc.docId); await problem.del(this.pdoc.domainId, this.pdoc.docId);
this.response.redirect = this.url('problem_main'); this.response.redirect = this.url('problem_main');
} }
@@ -264,7 +275,7 @@ export class ProblemSubmitHandler extends ProblemDetailHandler {
@param('lang', Types.Name) @param('lang', Types.Name)
@param('code', Types.Content) @param('code', Types.Content)
async post(domainId: string, lang: string, code: string) { async post(domainId: string, lang: string, code: string) {
if (this.response.body.pdoc.config.langs && !this.response.body.pdoc.config.langs.includes('lang')) { if (this.response.body.pdoc.config?.langs && !this.response.body.pdoc.config.langs.includes('lang')) {
throw new BadRequestError('Language not allowed.'); throw new BadRequestError('Language not allowed.');
} }
await this.limitRate('add_record', 60, 5); await this.limitRate('add_record', 60, 5);
+5
View File
@@ -444,6 +444,10 @@ export async function get(domainId: string, tid: ObjectID, type: Type | -1 = doc
return tdoc; return tdoc;
} }
export async function getRelated(domainId: string, pid: number, type: Type = document.TYPE_CONTEST) {
return await document.getMulti(domainId, type, { pids: pid }).toArray();
}
export async function updateStatus( export async function updateStatus(
domainId: string, tid: ObjectID, uid: number, rid: ObjectID, pid: number, domainId: string, tid: ObjectID, uid: number, rid: ObjectID, pid: number,
accept = false, score = 0, type: 30 | 60 = document.TYPE_CONTEST, accept = false, score = 0, type: 30 | 60 = document.TYPE_CONTEST,
@@ -624,6 +628,7 @@ global.Hydro.model.contest = {
edit, edit,
del, del,
get, get,
getRelated,
updateStatus, updateStatus,
getStatus, getStatus,
count, count,
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@hydrooj/ui-default", "name": "@hydrooj/ui-default",
"version": "4.8.32", "version": "4.8.33",
"author": "undefined <i@undefined.moe>", "author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "hydro.js", "main": "hydro.js",
@@ -251,7 +251,10 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
loadSubjective(); loadSubjective();
if (pagename === 'contest_detail_problem') return; if (pagename === 'contest_detail_problem') return;
UiContext.pdoc.stats = UiContext.pdoc.stats || {}; if (!Object.keys(UiContext.pdoc.stats || {}).length) {
$('#submission-status-placeholder').parent().hide();
return;
}
const $status = document.getElementById('submission-status-placeholder'); const $status = document.getElementById('submission-status-placeholder');
const statusChart = echarts.init($status); const statusChart = echarts.init($status);
statusChart.setOption({ statusChart.setOption({
@@ -105,14 +105,14 @@
</ol> </ol>
</div> </div>
</div> </div>
{% if page_name != 'problem_detail' and page_name != 'homework_detail_problem' %} {% if page_name != 'problem_detail' and page_name != 'homework_detail_problem' and page_name != 'contest_detail_problem' %}
<div class="section side"> <div class="section side">
<div class="section__header"> <div class="section__header">
<h1 class="section__title">{{ _('Information') }}</h1> <h1 class="section__title">{{ _('Information') }}</h1>
</div> </div>
{% include "partials/problem-sidebar-information.html" %} {% include "partials/problem-sidebar-information.html" %}
</div> </div>
{% else %} {% elif page_name != 'contest_detail_problem' %}
<div class="section side nojs-hide"> <div class="section side nojs-hide">
<div class="section__header"> <div class="section__header">
<h1 class="section__title">{{ _('Statistics') }}</h1> <h1 class="section__title">{{ _('Statistics') }}</h1>
@@ -121,19 +121,19 @@
<div id="submission-score-placeholder" style="width: 100%; height:230px"></div> <div id="submission-score-placeholder" style="width: 100%; height:230px"></div>
</div> </div>
{% endif %} {% endif %}
{% if page_name == 'problem_detail' and (tdocs or ctdocs) %} {% if page_name == 'problem_detail' and (tdocs.length or ctdocs.length) %}
<div class="section side"> <div class="section side">
<div class="section__header"> <div class="section__header">
<h1 class="section__title">{{ _('Related') }}</h1> <h1 class="section__title">{{ _('Related') }}</h1>
</div> </div>
<div class="section__body typo"> <div class="section__body typo">
{% if tdocs %} {% if tdocs.length %}
<p>{{ _('In following training plans') }}: </p> <p>{{ _('In following training plans') }}: </p>
{%- for tdoc in tdocs -%} {%- for tdoc in tdocs -%}
<p><a href="{{ url('training_detail', tid=tdoc.docId) }}">{{ tdoc.title }}</a></p> <p><a href="{{ url('training_detail', tid=tdoc.docId) }}">{{ tdoc.title }}</a></p>
{%- endfor -%} {%- endfor -%}
{% endif %} {% endif %}
{% if ctdocs %} {% if ctdocs.length %}
<p>{{ _('In following contests') }}: </p> <p>{{ _('In following contests') }}: </p>
{%- for tdoc in ctdocs -%} {%- for tdoc in ctdocs -%}
<p><a href="{{ url('contest_detail', tid=tdoc.docId) }}">{{ tdoc.title }}</a></p> <p><a href="{{ url('contest_detail', tid=tdoc.docId) }}">{{ tdoc.title }}</a></p>