mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
ui: download multiple problems
This commit is contained in:
1 parent
91d85c69a0
commit
bdf2d972cd
6 files changed
+87
-17
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydrooj",
|
"name": "hydrooj",
|
||||||
"version": "2.26.19",
|
"version": "2.26.20",
|
||||||
"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",
|
||||||
|
|||||||
@@ -339,19 +339,6 @@ export class ProblemPretestConnectionHandler extends ConnectionHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProblemStatisticsHandler extends ProblemDetailHandler {
|
|
||||||
async get(domainId: string) {
|
|
||||||
const udoc = await user.getById(domainId, this.pdoc.owner);
|
|
||||||
const path = [
|
|
||||||
['problem_main', 'problem_main'],
|
|
||||||
[this.pdoc.title, 'problem_detail', { pid: this.pdoc.pid || this.pdoc.docId }, true],
|
|
||||||
['problem_statistics', null],
|
|
||||||
];
|
|
||||||
this.response.template = 'problem_statistics.html';
|
|
||||||
this.response.body = { pdoc: this.pdoc, udoc, path };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ProblemManageHandler extends ProblemDetailHandler {
|
export class ProblemManageHandler extends ProblemDetailHandler {
|
||||||
async prepare() {
|
async prepare() {
|
||||||
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);
|
||||||
@@ -435,6 +422,10 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
|
|||||||
throw new ForbiddenError('File limit exceeded.');
|
throw new ForbiddenError('File limit exceeded.');
|
||||||
}
|
}
|
||||||
if (!this.request.files.file) throw new ValidationError('file');
|
if (!this.request.files.file) throw new ValidationError('file');
|
||||||
|
const size = Math.sum((this.pdoc.data || []).map((i) => i.size), (this.pdoc.additional_file || []).map((i) => i.size));
|
||||||
|
if (size >= system.get('limit.problem_files_max_size')) {
|
||||||
|
throw new ForbiddenError('File size limit exceeded.');
|
||||||
|
}
|
||||||
if (!filename) filename = this.request.files.file.name || String.random(16);
|
if (!filename) filename = this.request.files.file.name || String.random(16);
|
||||||
if (filename.includes('/') || filename.includes('..')) throw new ValidationError('filename', 'Bad filename');
|
if (filename.includes('/') || filename.includes('..')) throw new ValidationError('filename', 'Bad filename');
|
||||||
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);
|
||||||
@@ -648,7 +639,6 @@ export async function apply() {
|
|||||||
Route('problem_detail', '/p/:pid', ProblemDetailHandler, PERM.PERM_VIEW_PROBLEM);
|
Route('problem_detail', '/p/:pid', ProblemDetailHandler, PERM.PERM_VIEW_PROBLEM);
|
||||||
Route('problem_submit', '/p/:pid/submit', ProblemSubmitHandler, PERM.PERM_SUBMIT_PROBLEM);
|
Route('problem_submit', '/p/:pid/submit', ProblemSubmitHandler, PERM.PERM_SUBMIT_PROBLEM);
|
||||||
Route('problem_pretest', '/p/:pid/pretest', ProblemPretestHandler, PERM.PERM_SUBMIT_PROBLEM);
|
Route('problem_pretest', '/p/:pid/pretest', ProblemPretestHandler, PERM.PERM_SUBMIT_PROBLEM);
|
||||||
Route('problem_statistics', '/p/:pid/statistics', ProblemStatisticsHandler, PERM.PERM_VIEW_PROBLEM);
|
|
||||||
Route('problem_edit', '/p/:pid/edit', ProblemEditHandler);
|
Route('problem_edit', '/p/:pid/edit', ProblemEditHandler);
|
||||||
Route('problem_files', '/p/:pid/files', ProblemFilesHandler, PERM.PERM_VIEW_PROBLEM);
|
Route('problem_files', '/p/:pid/files', ProblemFilesHandler, PERM.PERM_VIEW_PROBLEM);
|
||||||
Route('problem_file_download', '/p/:pid/file/:filename', ProblemFileDownloadHandler, PERM.PERM_VIEW_PROBLEM);
|
Route('problem_file_download', '/p/:pid/file/:filename', ProblemFileDownloadHandler, PERM.PERM_VIEW_PROBLEM);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable max-len */
|
||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
import { Dictionary } from 'lodash';
|
import { Dictionary } from 'lodash';
|
||||||
@@ -182,6 +183,7 @@ SystemSetting(
|
|||||||
Setting('setting_server', 'server.language', 'zh_CN', langRange, 'server.language', 'Default display language'),
|
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', FLAG_PRO),
|
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_limits', 'limit.problem_files_max', 100, 'number', 'limit.problem_files_max', 'Max files per problem'),
|
||||||
|
Setting('setting_limits', 'limit.problem_files_size_max', 128 * 1024 * 1024, 'number', 'limit.problem_files_size_max', 'Max files size per problem'),
|
||||||
Setting('setting_basic', 'default.priv', builtin.PRIV.PRIV_DEFAULT, 'number', 'default.priv', 'Default Privilege', FLAG_PRO),
|
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', '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.problem', 100, 'number', 'pagination.problem', 'Problems per page'),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/ui-default",
|
"name": "@hydrooj/ui-default",
|
||||||
"version": "4.8.35",
|
"version": "4.8.36",
|
||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "hydro.js",
|
"main": "hydro.js",
|
||||||
|
|||||||
@@ -1,19 +1,38 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { dump } from 'js-yaml';
|
||||||
|
import * as streamsaver from 'streamsaver';
|
||||||
import { NamedPage } from 'vj/misc/Page';
|
import { NamedPage } from 'vj/misc/Page';
|
||||||
import Notification from 'vj/components/notification';
|
import Notification from 'vj/components/notification';
|
||||||
import { ConfirmDialog } from 'vj/components/dialog';
|
import { ConfirmDialog } from 'vj/components/dialog';
|
||||||
import Dropdown from 'vj/components/dropdown/Dropdown';
|
import Dropdown from 'vj/components/dropdown/Dropdown';
|
||||||
|
import { createZipStream } from 'vj/utils/zip';
|
||||||
import pjax from 'vj/utils/pjax';
|
import pjax from 'vj/utils/pjax';
|
||||||
import substitute from 'vj/utils/substitute';
|
import substitute from 'vj/utils/substitute';
|
||||||
import request from 'vj/utils/request';
|
import request from 'vj/utils/request';
|
||||||
import tpl from 'vj/utils/tpl';
|
import tpl from 'vj/utils/tpl';
|
||||||
|
import pipeStream from 'vj/utils/pipeStream';
|
||||||
import delay from 'vj/utils/delay';
|
import delay from 'vj/utils/delay';
|
||||||
import i18n from 'vj/utils/i18n';
|
import i18n from 'vj/utils/i18n';
|
||||||
|
|
||||||
const categories = {};
|
const categories = {};
|
||||||
const dirtyCategories = [];
|
const dirtyCategories = [];
|
||||||
const selections = [];
|
const selections = [];
|
||||||
|
// Firefox have no WritableStream
|
||||||
|
if (!window.WritableStream) streamsaver.WritableStream = WritableStream;
|
||||||
|
if (window.location.protocol === 'https:'
|
||||||
|
|| window.location.protocol === 'chrome-extension:'
|
||||||
|
|| window.location.hostname === 'localhost') {
|
||||||
|
streamsaver.mitm = '/streamsaver/mitm.html';
|
||||||
|
}
|
||||||
|
|
||||||
|
let isBeforeUnloadTriggeredByLibrary = !window.isSecureContext;
|
||||||
|
function onBeforeUnload(e) {
|
||||||
|
if (isBeforeUnloadTriggeredByLibrary) {
|
||||||
|
isBeforeUnloadTriggeredByLibrary = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.returnValue = '';
|
||||||
|
}
|
||||||
|
|
||||||
function setDomSelected($dom, selected) {
|
function setDomSelected($dom, selected) {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
@@ -202,6 +221,60 @@ async function handleOperation(operation) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleDownload() {
|
||||||
|
const pids = ensureAndGetSelectedPids();
|
||||||
|
const fileStream = streamsaver.createWriteStream('Export.zip');
|
||||||
|
const targets = [];
|
||||||
|
for (const pid of pids) {
|
||||||
|
const { pdoc } = await request.get(`p/${pid}`);
|
||||||
|
targets.push({ filename: `${pid}/problem.yaml`, content: dump(pdoc) });
|
||||||
|
let { links } = await request.post(`p/${pid}/files`, { operation: 'get_links', files: pdoc.data.map((i) => i.name), type: 'testdata' });
|
||||||
|
for (const filename of Object.keys(links)) {
|
||||||
|
targets.push({ filename: `${pid}/testdata/${filename}`, url: links[filename] });
|
||||||
|
}
|
||||||
|
({ links } = await request.post(`p/${pid}/files`, {
|
||||||
|
operation: 'get_links', files: pdoc.additional_file.map((i) => i.name), type: 'additional_file',
|
||||||
|
}));
|
||||||
|
for (const filename of Object.keys(links)) {
|
||||||
|
targets.push({ filename: `${pid}/additional_file/${filename}`, url: links[filename] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let i = 0;
|
||||||
|
const zipStream = createZipStream({
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
|
async pull(ctrl) {
|
||||||
|
if (i === targets.length) return ctrl.close();
|
||||||
|
try {
|
||||||
|
if (targets[i].url) {
|
||||||
|
const response = await fetch(targets[i].url);
|
||||||
|
if (!response.ok) throw response.statusText;
|
||||||
|
ctrl.enqueue({
|
||||||
|
name: targets[i].filename,
|
||||||
|
stream: () => response.body,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ctrl.enqueue({
|
||||||
|
name: targets[i].filename,
|
||||||
|
stream: () => new Blob([targets[i].content], { type: 'application/json' }).stream(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-use-before-define
|
||||||
|
stopDownload();
|
||||||
|
Notification.error(i18n('problem_files.download_as_archive_error', [targets[i].filename, e.toString()]));
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const abortCallbackReceiver = {};
|
||||||
|
function stopDownload() { abortCallbackReceiver.abort(); }
|
||||||
|
window.addEventListener('unload', stopDownload);
|
||||||
|
window.addEventListener('beforeunload', onBeforeUnload);
|
||||||
|
await pipeStream(zipStream, fileStream, abortCallbackReceiver);
|
||||||
|
window.removeEventListener('unload', stopDownload);
|
||||||
|
window.removeEventListener('beforeunload', onBeforeUnload);
|
||||||
|
}
|
||||||
|
|
||||||
const page = new NamedPage(['problem_main', 'problem_category'], () => {
|
const page = new NamedPage(['problem_main', 'problem_category'], () => {
|
||||||
const doc = document.documentElement;
|
const doc = document.documentElement;
|
||||||
doc.className += ' display-mode';
|
doc.className += ' display-mode';
|
||||||
@@ -216,6 +289,7 @@ const page = new NamedPage(['problem_main', 'problem_category'], () => {
|
|||||||
$('[name="remove_selected_problems"]').on('click', () => handleOperation('delete'));
|
$('[name="remove_selected_problems"]').on('click', () => handleOperation('delete'));
|
||||||
$('[name="hide_selected_problems"]').on('click', () => handleOperation('hide'));
|
$('[name="hide_selected_problems"]').on('click', () => handleOperation('hide'));
|
||||||
$('[name="unhide_selected_problems"]').on('click', () => handleOperation('unhide'));
|
$('[name="unhide_selected_problems"]').on('click', () => handleOperation('unhide'));
|
||||||
|
$('[name="download_selected_problems"]').on('click', handleDownload);
|
||||||
$('#search').on('click', (ev) => {
|
$('#search').on('click', (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
updateSelection();
|
updateSelection();
|
||||||
|
|||||||
@@ -36,6 +36,10 @@
|
|||||||
<span class="icon icon-check"></span>
|
<span class="icon icon-check"></span>
|
||||||
{{ _('Unhide Selected') }}
|
{{ _('Unhide Selected') }}
|
||||||
</a>
|
</a>
|
||||||
|
<a href="javascript:;" class="menu__link display-mode-hide" name="download_selected_problems">
|
||||||
|
<span class="icon icon-delete"></span>
|
||||||
|
{{ _('Download Selected') }}
|
||||||
|
</a>
|
||||||
<a href="javascript:;" class="menu__link display-mode-hide" name="remove_selected_problems">
|
<a href="javascript:;" class="menu__link display-mode-hide" name="remove_selected_problems">
|
||||||
<span class="icon icon-delete"></span>
|
<span class="icon icon-delete"></span>
|
||||||
{{ _('Remove Selected') }}
|
{{ _('Remove Selected') }}
|
||||||
|
|||||||
Reference in new issue
Block a user