mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
ui: problem_files: pjax support
This commit is contained in:
1 parent
c061c1ec93
commit
3e1d4635c4
16 files changed
+234
-184
No files matched your search
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydrooj",
|
||||
"version": "2.26.0",
|
||||
"version": "2.26.1",
|
||||
"bin": "bin/hydrooj.js",
|
||||
"main": "dist/loader.js",
|
||||
"typings": "dist/loader.d.ts",
|
||||
|
||||
@@ -92,6 +92,7 @@ export const ValidationError = Err('ValidationError', ForbiddenError, function (
|
||||
return 'Field {0} or {1} validation failed.';
|
||||
});
|
||||
export const ContestNotAttendedError = Err('ContestNotAttendedError', ForbiddenError, "You haven't attended this contest yet.");
|
||||
export const RequireProError = Err('RequireProError', ForbiddenError, 'RequireProError');
|
||||
export const ContestAlreadyAttendedError = Err('ContestAlreadyAttendedError', ForbiddenError, "You've already attended this contest.");
|
||||
export const ContestNotLiveError = Err('ContestNotLiveError', ForbiddenError, 'This contest is not live.');
|
||||
export const ContestScoreboardHiddenError = Err('ContestScoreboardHiddenError', ForbiddenError, 'Contest scoreboard is not visible.');
|
||||
@@ -142,6 +143,7 @@ global.Hydro.error = {
|
||||
OpcountExceededError,
|
||||
PermissionError,
|
||||
PrivilegeError,
|
||||
RequireProError,
|
||||
NoProblemError,
|
||||
ValidationError,
|
||||
ProblemNotFoundError,
|
||||
|
||||
@@ -379,13 +379,24 @@ export class ProblemEditHandler extends ProblemManageHandler {
|
||||
export class ProblemFilesHandler extends ProblemDetailHandler {
|
||||
@param('testdata', Types.Boolean)
|
||||
@param('additional_file', Types.Boolean)
|
||||
async get(domainId: string, getTestdata = true, getAdditionalFile = true) {
|
||||
@param('pjax', Types.Boolean)
|
||||
async get(domainId: string, getTestdata = true, getAdditionalFile = true, pjax = false) {
|
||||
const canReadData = this.user.own(this.pdoc)
|
||||
|| this.user.hasPriv(PRIV.PRIV_READ_PROBLEM_DATA)
|
||||
|| this.user.hasPerm(PERM.PERM_READ_PROBLEM_DATA);
|
||||
this.response.body.testdata = (getTestdata && canReadData) ? sortFiles(this.pdoc.data || []) : [];
|
||||
this.response.body.additional_file = getAdditionalFile ? sortFiles(this.pdoc.additional_file || []) : [];
|
||||
this.response.template = 'problem_files.html';
|
||||
if (pjax) {
|
||||
const { testdata, additional_file } = this.response.body;
|
||||
this.response.body = {
|
||||
fragments: (await Promise.all([
|
||||
this.renderHTML('partials/problem_files-testdata.html', { testdata, pdoc: this.pdoc }),
|
||||
this.renderHTML('partials/problem_files-additional_file.html', { additional_file, pdoc: this.pdoc }),
|
||||
this.renderHTML('partials/problem-sidebar-information.html', { pdoc: this.pdoc }),
|
||||
])).map((i) => ({ html: i })),
|
||||
};
|
||||
this.response.template = '';
|
||||
} else this.response.template = 'problem_files.html';
|
||||
}
|
||||
|
||||
@post('files', Types.Set)
|
||||
|
||||
@@ -28,6 +28,7 @@ for (const lang in global.Hydro.locales) {
|
||||
export const FLAG_HIDDEN = 1;
|
||||
export const FLAG_DISABLED = 2;
|
||||
export const FLAG_SECRET = 4;
|
||||
export const FLAG_PRO = 8;
|
||||
|
||||
export const PREFERENCE_SETTINGS: _Setting[] = [];
|
||||
export const ACCOUNT_SETTINGS: _Setting[] = [];
|
||||
@@ -167,7 +168,8 @@ SystemSetting(
|
||||
Setting('setting_server', 'server.center', 'https://hydro.undefined.moe:8443/center', 'text', 'server.center', '', FLAG_HIDDEN),
|
||||
Setting('setting_server', 'server.proxy', false, 'boolean', 'server.proxy', 'Use hydro center proxy to send problems'),
|
||||
Setting('setting_server', 'server.name', 'Hydro', 'text', 'server.name', 'Server Name'),
|
||||
Setting('setting_server', 'server.worker', 1, 'number', 'server.worker', 'Server Workers Number'),
|
||||
Setting('setting_server', 'server.displayName', 'Hydro', 'text', 'server.name', 'Server Name (Global Display)', FLAG_PRO),
|
||||
Setting('setting_server', 'server.worker', 1, 'number', 'server.worker', 'Server Workers Number', FLAG_PRO),
|
||||
Setting('setting_server', 'server.hostname', 'oj.undefined.moe', 'text', 'server.hostname', 'Server Hostname'),
|
||||
Setting('setting_server', 'server.host', 'oj.undefined.moe', 'text', 'server.host', 'Server Host'),
|
||||
Setting('setting_server', 'server.url', '/', 'text', 'server.url', 'Server BaseURL'),
|
||||
@@ -176,9 +178,9 @@ SystemSetting(
|
||||
Setting('setting_server', 'server.xff', null, 'text', 'server.xff', 'IP Header'),
|
||||
Setting('setting_server', 'server.xhost', null, 'text', 'server.xhost', 'Hostname Header'),
|
||||
Setting('setting_server', 'server.language', 'zh_CN', langRange, 'server.language', 'Default display language'),
|
||||
Setting('setting_server', 'server.login', true, 'boolean', 'server.login', 'Allow builtin-login'),
|
||||
Setting('setting_server', 'server.login', true, 'boolean', 'server.login', 'Allow builtin-login', FLAG_PRO),
|
||||
Setting('setting_limits', 'limit.problem_files_max', 100, 'number', 'limit.problem_files_max', 'Max files per problem'),
|
||||
Setting('setting_basic', 'default.priv', builtin.PRIV.PRIV_DEFAULT, 'number', 'default.priv', 'Default Privilege'),
|
||||
Setting('setting_basic', 'default.priv', builtin.PRIV.PRIV_DEFAULT, 'number', 'default.priv', 'Default Privilege', FLAG_PRO),
|
||||
Setting('setting_basic', 'problem.categories', builtin.CATEGORIES, 'yaml', 'problem.categories', 'Problem Categories'),
|
||||
Setting('setting_basic', 'pagination.problem', 100, 'number', 'pagination.problem', 'Problems per page'),
|
||||
Setting('setting_basic', 'pagination.contest', 20, 'number', 'pagination.contest', 'Contests per page'),
|
||||
@@ -244,6 +246,7 @@ global.Hydro.model.setting = {
|
||||
FLAG_HIDDEN,
|
||||
FLAG_DISABLED,
|
||||
FLAG_SECRET,
|
||||
FLAG_PRO,
|
||||
PREFERENCE_SETTINGS,
|
||||
ACCOUNT_SETTINGS,
|
||||
SETTINGS,
|
||||
|
||||
@@ -50,7 +50,7 @@ export class StorageModel {
|
||||
|
||||
static async list(target: string, recursive = true) {
|
||||
if (target.includes('..') || target.includes('//')) throw new Error('Invalid path');
|
||||
if (target?.length && !target.endsWith('/')) target += '/';
|
||||
if (target.length && !target.endsWith('/')) target += '/';
|
||||
const results = recursive
|
||||
? await StorageModel.coll.find({ path: { $regex: new RegExp(`^${escapeRegExp(target)}`, 'i') } }).toArray()
|
||||
: await StorageModel.coll.find({ path: { $regex: new RegExp(`^${escapeRegExp(target)}[^/]+$`) } }).toArray();
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
import { Position, Toaster, Intent } from '@blueprintjs/core';
|
||||
import tpl from 'vj/utils/tpl';
|
||||
import zIndexManager from 'vj/utils/zIndexManager';
|
||||
|
||||
const ToasterContainer = document.createElement('div');
|
||||
ToasterContainer.style.position = 'fixed';
|
||||
ToasterContainer.style.bottom = '0px';
|
||||
ToasterContainer.style.zIndex = '9999';
|
||||
document.body.append(ToasterContainer);
|
||||
|
||||
export const AppToaster = Toaster.create({
|
||||
className: 'recipe-toaster',
|
||||
position: Position.LEFT_BOTTOM,
|
||||
usePortal: true,
|
||||
}, ToasterContainer);
|
||||
|
||||
export default class Notification {
|
||||
constructor({
|
||||
avatar, title, message, type = '', duration = 3000, action,
|
||||
@@ -37,19 +50,19 @@ export default class Notification {
|
||||
}
|
||||
|
||||
static success(message, duration) {
|
||||
return new Notification({ type: 'success', message, duration }).show();
|
||||
return AppToaster.show({ message, timeout: duration, intent: Intent.SUCCESS });
|
||||
}
|
||||
|
||||
static info(message, duration) {
|
||||
return new Notification({ type: 'info', message, duration }).show();
|
||||
return AppToaster.show({ message, timeout: duration, intent: Intent.PRIMARY });
|
||||
}
|
||||
|
||||
static warn(message, duration) {
|
||||
return new Notification({ type: 'warn', message, duration }).show();
|
||||
return AppToaster.show({ message, timeout: duration, intent: Intent.WARNING });
|
||||
}
|
||||
|
||||
static error(message, duration) {
|
||||
return new Notification({ type: 'error', message, duration }).show();
|
||||
return AppToaster.show({ message, timeout: duration, intent: Intent.DANGER });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES6",
|
||||
"baseUrl": ".",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"vj/*": [
|
||||
"./*"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/ui-default",
|
||||
"version": "4.8.13",
|
||||
"version": "4.8.14",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "hydro.js",
|
||||
@@ -22,6 +22,8 @@
|
||||
"@babel/preset-react": "^7.13.13",
|
||||
"@babel/register": "^7.13.14",
|
||||
"@babel/runtime-corejs3": "^7.14.0",
|
||||
"@blueprintjs/core": "^3.44.2",
|
||||
"@blueprintjs/icons": "^3.26.0",
|
||||
"@hydrooj/utils": "^1.0.15",
|
||||
"@undefined-moe/monaco-yaml": "^2.5.0",
|
||||
"ansi_up": "^5.0.1",
|
||||
|
||||
@@ -6,9 +6,9 @@ import { NamedPage } from 'vj/misc/Page';
|
||||
import Notification from 'vj/components/notification';
|
||||
import { ConfirmDialog, ActionDialog } from 'vj/components/dialog/index';
|
||||
import request from 'vj/utils/request';
|
||||
import pjax from 'vj/utils/pjax';
|
||||
import pipeStream from 'vj/utils/pipeStream';
|
||||
import tpl from 'vj/utils/tpl';
|
||||
import delay from 'vj/utils/delay';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
|
||||
// Firefox have no WritableStream
|
||||
@@ -96,8 +96,7 @@ const page = new NamedPage('problem_files', () => {
|
||||
}
|
||||
await Promise.all(tasks);
|
||||
Notification.success('File uploaded successfully.');
|
||||
await delay(2000);
|
||||
window.location.reload();
|
||||
await pjax.request({ push: false });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Notification.error(`File upload fail: ${e.toString()}`);
|
||||
@@ -127,8 +126,7 @@ const page = new NamedPage('problem_files', () => {
|
||||
type,
|
||||
});
|
||||
Notification.success(i18n('Selected files have been deleted.'));
|
||||
await delay(2000);
|
||||
window.location.reload();
|
||||
await pjax.request({ push: false });
|
||||
} catch (error) {
|
||||
Notification.error(error.message);
|
||||
}
|
||||
@@ -189,14 +187,22 @@ const page = new NamedPage('problem_files', () => {
|
||||
* @param {JQuery.ClickEvent<HTMLElement, undefined, HTMLElement, HTMLElement>} ev
|
||||
*/
|
||||
async function handleEdit(type, ev) {
|
||||
ev.preventDefault();
|
||||
const filename = ev.currentTarget.closest('[data-filename]').getAttribute('data-filename');
|
||||
const filesize = +ev.currentTarget.closest('[data-size]').getAttribute('data-size');
|
||||
const link = $(ev.currentTarget).find('a').attr('href');
|
||||
if (filesize > 8 * 1024 * 1024) Notification.error(i18n('file too large'));
|
||||
Notification.info(i18n('Loading file...'));
|
||||
const res = await request.get(link);
|
||||
const content = await request.get(res.url, undefined, { dataType: 'text' });
|
||||
if (ev) ev.preventDefault();
|
||||
const filename = ev
|
||||
? ev.currentTarget.closest('[data-filename]').getAttribute('data-filename')
|
||||
// eslint-disable-next-line no-alert
|
||||
: prompt('Filename');
|
||||
const filesize = ev
|
||||
? +ev.currentTarget.closest('[data-size]').getAttribute('data-size')
|
||||
: 0;
|
||||
let content = '';
|
||||
if (ev) {
|
||||
const link = $(ev.currentTarget).find('a').attr('href');
|
||||
if (filesize > 8 * 1024 * 1024) Notification.error(i18n('file too large'));
|
||||
Notification.info(i18n('Loading file...'));
|
||||
const res = await request.get(link);
|
||||
content = await request.get(res.url, undefined, { dataType: 'text' });
|
||||
} else Notification.info(i18n('Loading editor...'));
|
||||
const val = await startEdit(filename, content);
|
||||
console.log(val);
|
||||
if (typeof val !== 'string') return;
|
||||
@@ -208,8 +214,7 @@ const page = new NamedPage('problem_files', () => {
|
||||
data.append('operation', 'upload_file');
|
||||
await request.postFile('', data);
|
||||
Notification.success(i18n('File saved.'));
|
||||
await delay(2000);
|
||||
window.location.reload();
|
||||
await pjax.request({ push: false });
|
||||
}
|
||||
|
||||
$('.problem-files-testdata .col--name').on('click', (ev) => handleEdit('testdata', ev));
|
||||
@@ -219,9 +224,11 @@ const page = new NamedPage('problem_files', () => {
|
||||
$('.problem-files-testdata').on('drop', (ev) => handleDrop('testdata', ev));
|
||||
$('.problem-files-additional_file').on('drop', (ev) => handleDrop('additional_file', ev));
|
||||
$('[name="upload_testdata"]').on('click', () => handleClickUpload('testdata'));
|
||||
$('[name="create_testdata"]').on('click', () => handleEdit('testdata'));
|
||||
$('[name="download_selected_testdata"]').on('click', () => handleClickDownloadSelected('testdata'));
|
||||
$('[name="remove_selected_testdata"]').on('click', () => handleClickRemoveSelected('testdata'));
|
||||
$('[name="upload_file"]').on('click', () => handleClickUpload('additional_file'));
|
||||
$('[name="create_file"]').on('click', () => handleEdit('additional_file'));
|
||||
$('[name="download_selected_file"]').on('click', () => handleClickDownloadSelected('additional_file'));
|
||||
$('[name="remove_selected_file"]').on('click', () => handleClickRemoveSelected('additional_file'));
|
||||
});
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
{% import "components/record.html" as record with context %}
|
||||
{% import "components/problem.html" as problem with context %}
|
||||
{% import "components/user.html" as user with context %}
|
||||
<div class="section__body typo" data-fragment-id="problem-sidebar-information">
|
||||
<dl class="large horizontal">
|
||||
<dt>{{ _('ID') }}</dt>
|
||||
<dd>{{ pdoc.docId }}</dd>
|
||||
<dt>{{ _('Time') }}</dt>
|
||||
<dd>{{ pdoc.config.timeMax if pdoc.config.timeMax == pdoc.config.timeMin else pdoc.config.timeMin+'~'+pdoc.config.timeMax }}ms</dd>
|
||||
<dt>{{ _('Memory') }}</dt>
|
||||
<dd>{{ pdoc.config.memoryMax if pdoc.config.memoryMax == pdoc.config.memoryMin else pdoc.config.memoryMin+'~'+pdoc.config.memoryMax }}MiB</dd>
|
||||
{% if page_name != 'discussion_node' and page_name != 'discussion_detail' %}
|
||||
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
|
||||
<dt>{{ _('Submission') }}</dt>
|
||||
<dd>
|
||||
{% if pdoc.psdoc.status %}
|
||||
<span class="icon record-status--icon {{ model.builtin.STATUS_CODES[pdoc['psdoc']['status']] }}"></span>
|
||||
<a href="{{ url('record_detail', rid=pdoc.psdoc.rid) }}" class="record-status--text {{ model.builtin.STATUS_CODES[pdoc['psdoc']['status']] }}">
|
||||
{{ model.builtin.STATUS_TEXTS[pdoc['psdoc']['status']] }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ _('(None)') }}
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<dt>{{ _('Difficulty') }}</dt>
|
||||
<dd>{{ pdoc['difficulty'] or _('(None)') }}</dd>
|
||||
<dt>{{ _('Category') }}</dt>
|
||||
<dd>
|
||||
{% if pdoc['category'] %}
|
||||
<span class="hasjs--hide" name="problem-sidebar__categories">
|
||||
{%- for category in pdoc['category'] -%}
|
||||
<a href="{{ url('problem_category', category=category) }}">{{ category }}</a>
|
||||
{% if loop.index < pdoc['category']|length %}
|
||||
{{ _('problem-subcategory-delim') }}
|
||||
{% endif %}
|
||||
{%- endfor -%}
|
||||
</span>
|
||||
<span class="nojs--hide">
|
||||
<a href="javascript:;" name="problem-sidebar__show-category">{{ _('page.problem_detail.sidebar.show_category') }}</a>
|
||||
</span>
|
||||
{% else %}
|
||||
{{ _('(None)') }}
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt>{{ _('Tags') }}</dt>
|
||||
<dd>
|
||||
{{ problem.render_problem_tags(pdoc, true) }}
|
||||
</dd>
|
||||
<dt>{{ _('# Submissions') }}</dt>
|
||||
<dd><a href="{{ url('record_main', query={pid:pdoc.docId}) }}">{{ pdoc.nSubmit }}</a></dd>
|
||||
{% if page_name != 'discussion_node' and page_name != 'discussion_detail' %}
|
||||
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
|
||||
<dt>{{ _('# My Submissions') }}</dt>
|
||||
<dd><a href="{{ url('record_main', query={pid:pdoc.docId, uidOrName:handler.user._id}) }}">{{ pdoc['psdoc'].nSubmit|default(0) }}</a></dd>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<dt>{{ _('Accepted') }}</dt>
|
||||
<dd>{{ pdoc.nAccept }}</dd>
|
||||
<dt>{{ _('Accepted Ratio') }}</dt>
|
||||
<dd>{% if pdoc.nSubmit > 0 %}{{ (100 * pdoc.nAccept / pdoc.nSubmit)|round|int }}%{% else %}{{ _('?') }}{% endif %}</dd>
|
||||
<dt>{{ _('Uploaded By') }}</dt>
|
||||
<dd>{{ user.render_inline(owner_udoc, badge=false) }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
{% import "components/nothing.html" as nothing with context %}
|
||||
<div class="section__body no-padding problem-files-additional_file" data-fragment-id="problem-files-additional_file">
|
||||
{% if additional_file.length %}
|
||||
<table class="data-table">
|
||||
<colgroup>
|
||||
<col class="col--checkbox">
|
||||
<col class="col--name">
|
||||
<col class="col--size">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col--checkbox">
|
||||
<label class="compact checkbox">
|
||||
<input type="checkbox" name="select_all" data-checkbox-toggle="additional_file">
|
||||
</label>
|
||||
</th>
|
||||
<th class="col--name">{{ _('Filename') }}</th>
|
||||
<th class="col--size">{{ _('Size') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for file in additional_file -%}
|
||||
<tr data-filename="{{ file.name }}" data-size="{{ file.size }}">
|
||||
<td class="col--checkbox">
|
||||
<label class="compact checkbox">
|
||||
<input type="checkbox" data-checkbox-group="additional_file" data-checkbox-range>
|
||||
</label>
|
||||
</td>
|
||||
<td class="col--name">
|
||||
<a href="{{ url('problem_file_download', {pid: pdoc.docId, filename: file.name}) }}">{{ file.name }}</a>
|
||||
</td>
|
||||
<td class="col--size">{{ size(file.size) }}</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
{{ nothing.render('No additional file at current.') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -0,0 +1,44 @@
|
||||
{% import "components/nothing.html" as nothing with context %}
|
||||
<div class="section__body no-padding problem-files-testdata" data-fragment-id="problem-files-testdata">
|
||||
{% if testdata.length %}
|
||||
<table class="data-table">
|
||||
<colgroup>
|
||||
<col class="col--checkbox">
|
||||
<col class="col--name">
|
||||
<col class="col--size">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col--checkbox">
|
||||
<label class="compact checkbox">
|
||||
<input type="checkbox" name="select_all" data-checkbox-toggle="testdata">
|
||||
</label>
|
||||
</th>
|
||||
<th class="col--name">{{ _('Filename') }}</th>
|
||||
<th class="col--size">{{ _('Size') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for file in testdata -%}
|
||||
<tr data-filename="{{ file.name }}" data-size="{{ file.size }}">
|
||||
<td class="col--checkbox">
|
||||
<label class="compact checkbox">
|
||||
<input type="checkbox" data-checkbox-group="testdata" data-checkbox-range>
|
||||
</label>
|
||||
</td>
|
||||
<td class="col--name">
|
||||
<a href="{{ url('problem_file_download', {pid: pdoc.docId, filename: file.name, query:{type:'testdata'} }) }}">{{ file.name }}</a>
|
||||
</td>
|
||||
<td class="col--size">{{ size(file.size) }}</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
{% if handler.user.own(pdoc) or handler.user.hasPriv(PRIV.PRIV_READ_PROBLEM_DATA) or handler.user.hasPerm(perm.PERM_READ_PROBLEM_DATA) %}
|
||||
{{ nothing.render('No testdata at current.') }}
|
||||
{% else %}
|
||||
{{ nothing.render("You don't have permission to read testdata.")}}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -110,69 +110,7 @@
|
||||
<div class="section__header">
|
||||
<h1 class="section__title">{{ _('Information') }}</h1>
|
||||
</div>
|
||||
<div class="section__body typo">
|
||||
<dl class="large horizontal">
|
||||
<dt>{{ _('ID') }}</dt>
|
||||
<dd>{{ pdoc.docId }}</dd>
|
||||
<dt>{{ _('Time') }}</dt>
|
||||
<dd>{{ pdoc.config.timeMax if pdoc.config.timeMax == pdoc.config.timeMin else pdoc.config.timeMin+'~'+pdoc.config.timeMax }}ms</dd>
|
||||
<dt>{{ _('Memory') }}</dt>
|
||||
<dd>{{ pdoc.config.memoryMax if pdoc.config.memoryMax == pdoc.config.memoryMin else pdoc.config.memoryMin+'~'+pdoc.config.memoryMax }}MiB</dd>
|
||||
{% if page_name != 'discussion_node' and page_name != 'discussion_detail' %}
|
||||
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
|
||||
<dt>{{ _('Submission') }}</dt>
|
||||
<dd>
|
||||
{% if pdoc.psdoc.status %}
|
||||
<span class="icon record-status--icon {{ model.builtin.STATUS_CODES[pdoc['psdoc']['status']] }}"></span>
|
||||
<a href="{{ url('record_detail', rid=pdoc.psdoc.rid) }}" class="record-status--text {{ model.builtin.STATUS_CODES[pdoc['psdoc']['status']] }}">
|
||||
{{ model.builtin.STATUS_TEXTS[pdoc['psdoc']['status']] }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ _('(None)') }}
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<dt>{{ _('Difficulty') }}</dt>
|
||||
<dd>{{ pdoc['difficulty'] or _('(None)') }}</dd>
|
||||
<dt>{{ _('Category') }}</dt>
|
||||
<dd>
|
||||
{% if pdoc['category'] %}
|
||||
<span class="hasjs--hide" name="problem-sidebar__categories">
|
||||
{%- for category in pdoc['category'] -%}
|
||||
<a href="{{ url('problem_category', category=category) }}">{{ category }}</a>
|
||||
{% if loop.index < pdoc['category']|length %}
|
||||
{{ _('problem-subcategory-delim') }}
|
||||
{% endif %}
|
||||
{%- endfor -%}
|
||||
</span>
|
||||
<span class="nojs--hide">
|
||||
<a href="javascript:;" name="problem-sidebar__show-category">{{ _('page.problem_detail.sidebar.show_category') }}</a>
|
||||
</span>
|
||||
{% else %}
|
||||
{{ _('(None)') }}
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt>{{ _('Tags') }}</dt>
|
||||
<dd>
|
||||
{{ problem.render_problem_tags(pdoc, true) }}
|
||||
</dd>
|
||||
<dt>{{ _('# Submissions') }}</dt>
|
||||
<dd><a href="{{ url('record_main', query={pid:pdoc.docId}) }}">{{ pdoc.nSubmit }}</a></dd>
|
||||
{% if page_name != 'discussion_node' and page_name != 'discussion_detail' %}
|
||||
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
|
||||
<dt>{{ _('# My Submissions') }}</dt>
|
||||
<dd><a href="{{ url('record_main', query={pid:pdoc.docId, uidOrName:handler.user._id}) }}">{{ pdoc['psdoc'].nSubmit|default(0) }}</a></dd>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<dt>{{ _('Accepted') }}</dt>
|
||||
<dd>{{ pdoc.nAccept }}</dd>
|
||||
<dt>{{ _('Accepted Ratio') }}</dt>
|
||||
<dd>{% if pdoc.nSubmit > 0 %}{{ (100 * pdoc.nAccept / pdoc.nSubmit)|round|int }}%{% else %}{{ _('?') }}{% endif %}</dd>
|
||||
<dt>{{ _('Uploaded By') }}</dt>
|
||||
<dd>{{ user.render_inline(owner_udoc, badge=false) }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
{% include "partials/problem-sidebar-information.html" %}
|
||||
</div>
|
||||
{% if page_name == 'problem_detail' and (tdocs or ctdocs) %}
|
||||
<div class="section side">
|
||||
|
||||
@@ -7,54 +7,13 @@
|
||||
<h1 class="section__title">{{ _('Testdata') }}</h1>
|
||||
<div class="section__tools">
|
||||
{% if handler.user.own(pdoc, perm.PERM_EDIT_PROBLEM_SELF) or handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) %}
|
||||
<button class="primary rounded button" name="upload_testdata">{{ _('Add File') }}</button>
|
||||
<button class="primary rounded button" name="create_testdata">{{ _('Create File') }}</button>
|
||||
<button class="primary rounded button" name="upload_testdata">{{ _('Upload File') }}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{{ noscript_note.render() }}
|
||||
<div class="section__body no-padding problem-files-testdata">
|
||||
{% if testdata.length %}
|
||||
<table class="data-table">
|
||||
<colgroup>
|
||||
<col class="col--checkbox">
|
||||
<col class="col--name">
|
||||
<col class="col--size">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col--checkbox">
|
||||
<label class="compact checkbox">
|
||||
<input type="checkbox" name="select_all" data-checkbox-toggle="testdata">
|
||||
</label>
|
||||
</th>
|
||||
<th class="col--name">{{ _('Filename') }}</th>
|
||||
<th class="col--size">{{ _('Size') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for file in testdata -%}
|
||||
<tr data-filename="{{ file.name }}" data-size="{{ file.size }}">
|
||||
<td class="col--checkbox">
|
||||
<label class="compact checkbox">
|
||||
<input type="checkbox" data-checkbox-group="testdata" data-checkbox-range>
|
||||
</label>
|
||||
</td>
|
||||
<td class="col--name">
|
||||
<a href="{{ url('problem_file_download', {pid: pdoc.docId, filename: file.name, query:{type:'testdata'} }) }}">{{ file.name }}</a>
|
||||
</td>
|
||||
<td class="col--size">{{ size(file.size) }}</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
{% if handler.user.own(pdoc) or handler.user.hasPriv(PRIV.PRIV_READ_PROBLEM_DATA) or handler.user.hasPerm(perm.PERM_READ_PROBLEM_DATA) %}
|
||||
{{ nothing.render('No testdata at current.') }}
|
||||
{% else %}
|
||||
{{ nothing.render("You don't have permission to read testdata.")}}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include "partials/problem_files-testdata.html" %}
|
||||
{% if handler.user.own(pdoc, perm.PERM_EDIT_PROBLEM_SELF) or handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) %}
|
||||
{% if testdata.length %}
|
||||
<div class="section__body">
|
||||
@@ -71,49 +30,12 @@
|
||||
<h1 class="section__title">{{ _('Additional Files') }}</h1>
|
||||
<div class="section__tools">
|
||||
{% if handler.user.own(pdoc, perm.PERM_EDIT_PROBLEM_SELF) or handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) %}
|
||||
<button class="primary rounded button" name="upload_file">{{ _('Add File') }}</button>
|
||||
<button class="primary rounded button" name="create_file">{{ _('Create File') }}</button>
|
||||
<button class="primary rounded button" name="upload_file">{{ _('Upload File') }}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="section__body no-padding problem-files-additional_file">
|
||||
{% if additional_file.length %}
|
||||
<table class="data-table">
|
||||
<colgroup>
|
||||
<col class="col--checkbox">
|
||||
<col class="col--name">
|
||||
<col class="col--size">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col--checkbox">
|
||||
<label class="compact checkbox">
|
||||
<input type="checkbox" name="select_all" data-checkbox-toggle="additional_file">
|
||||
</label>
|
||||
</th>
|
||||
<th class="col--name">{{ _('Filename') }}</th>
|
||||
<th class="col--size">{{ _('Size') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for file in additional_file -%}
|
||||
<tr data-filename="{{ file.name }}" data-size="{{ file.size }}">
|
||||
<td class="col--checkbox">
|
||||
<label class="compact checkbox">
|
||||
<input type="checkbox" data-checkbox-group="additional_file" data-checkbox-range>
|
||||
</label>
|
||||
</td>
|
||||
<td class="col--name">
|
||||
<a href="{{ url('problem_file_download', {pid: pdoc.docId, filename: file.name}) }}">{{ file.name }}</a>
|
||||
</td>
|
||||
<td class="col--size">{{ size(file.size) }}</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
{{ nothing.render('No additional file at current.') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include "partials/problem_files-additional_file.html" %}
|
||||
{% if additional_file.length %}
|
||||
<div class="section__body">
|
||||
<button class="rounded button" name="download_selected_file">{{ _('Download Selected') }}</button>
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'pickadate/lib/themes/classic.css';
|
||||
import 'pickadate/lib/themes/classic.date.css';
|
||||
import 'pickadate/lib/themes/classic.time.css';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import '@blueprintjs/core/lib/css/blueprint.css';
|
||||
import '@blueprintjs/icons/lib/css/blueprint-icons.css';
|
||||
|
||||
import 'vj/misc/float.styl';
|
||||
import 'vj/misc/typography.styl';
|
||||
|
||||
@@ -70,12 +70,15 @@ pjax.request = async (opt) => {
|
||||
}
|
||||
incProgress();
|
||||
try {
|
||||
const data = await request.ajax({
|
||||
const params = {
|
||||
beforeSend(xhr) {
|
||||
currentXHR = xhr;
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
if (params.url && params.url.includes('?')) params.url += '&pjax=1';
|
||||
else params.url = `${params.url || ''}?pjax=1`;
|
||||
const data = await request.ajax(params);
|
||||
const meta = extractMetaData(options, data);
|
||||
currentState = {
|
||||
id: uuid(),
|
||||
@@ -84,7 +87,7 @@ pjax.request = async (opt) => {
|
||||
if (options.push) {
|
||||
window.history.replaceState(currentState, null, meta.url);
|
||||
}
|
||||
document.title = meta.title;
|
||||
if (meta.title) document.title = meta.title;
|
||||
data.fragments.forEach((fragment) => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (fragment.html === undefined) {
|
||||
@@ -120,7 +123,7 @@ pjax.request = async (opt) => {
|
||||
} catch (err) {
|
||||
if (!err.aborted) {
|
||||
Notification.error(err.message);
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
}
|
||||
} finally {
|
||||
decProgress();
|
||||
@@ -128,13 +131,9 @@ pjax.request = async (opt) => {
|
||||
};
|
||||
|
||||
function handlePopState(ev) {
|
||||
if (!ev.originalEvent) {
|
||||
return;
|
||||
}
|
||||
if (!ev.originalEvent) return;
|
||||
const { state } = ev.originalEvent;
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
if (!state) return;
|
||||
if (!state.id || (currentState && state.id === currentState.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user