mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 17:36:08 +08:00
core&ui: add homework files & enhance delete operation (#755)
This commit is contained in:
1 parent
61e56adead
commit
f0219116f3
12 files changed
+175
-13
No files matched your search
@@ -515,6 +515,7 @@ export class ContestEditHandler extends Handler {
|
||||
ScheduleModel.deleteMany({
|
||||
type: 'schedule', subType: 'contest', domainId, tid,
|
||||
}),
|
||||
storage.del(this.tdoc.files?.map((i) => `contest/${domainId}/${tid}/${i.name}`) || [], this.user._id),
|
||||
]));
|
||||
this.response.redirect = this.url('contest_main');
|
||||
}
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import { statSync } from 'fs-extra';
|
||||
import yaml from 'js-yaml';
|
||||
import { pick } from 'lodash';
|
||||
import moment from 'moment-timezone';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { Time } from '@hydrooj/utils/lib/utils';
|
||||
import { sortFiles, Time } from '@hydrooj/utils/lib/utils';
|
||||
import {
|
||||
ContestNotFoundError, HomeworkNotLiveError, NotAssignedError, ValidationError,
|
||||
ContestNotFoundError, FileLimitExceededError, FileUploadError, HomeworkNotLiveError, NotAssignedError, ValidationError,
|
||||
} from '../error';
|
||||
import { PenaltyRules } from '../interface';
|
||||
import { PenaltyRules, Tdoc } from '../interface';
|
||||
import { PERM } from '../model/builtin';
|
||||
import * as contest from '../model/contest';
|
||||
import * as discussion from '../model/discussion';
|
||||
import * as oplog from '../model/oplog';
|
||||
import problem from '../model/problem';
|
||||
import record from '../model/record';
|
||||
import storage from '../model/storage';
|
||||
import * as system from '../model/system';
|
||||
import user from '../model/user';
|
||||
import { Handler, param, Types } from '../service/server';
|
||||
import {
|
||||
Handler, param, post, Types,
|
||||
} from '../service/server';
|
||||
import { ContestCodeHandler, ContestScoreboardHandler } from './contest';
|
||||
|
||||
const validatePenaltyRules = (input: string) => yaml.load(input);
|
||||
@@ -22,7 +29,7 @@ class HomeworkMainHandler extends Handler {
|
||||
@param('group', Types.Name, true)
|
||||
@param('page', Types.PositiveInt, true)
|
||||
async get(domainId: string, group = '', page = 1) {
|
||||
const groups = (await user.listGroup(domainId, this.user.hasPerm(PERM.PERM_VIEW_HIDDEN_CONTEST) ? undefined : this.user._id))
|
||||
const groups = (await user.listGroup(domainId, this.user.hasPerm(PERM.PERM_VIEW_HIDDEN_HOMEWORK) ? undefined : this.user._id))
|
||||
.map((i) => i.name);
|
||||
if (group && !groups.includes(group)) throw new NotAssignedError(group);
|
||||
const cursor = contest.getMulti(domainId, {
|
||||
@@ -93,6 +100,9 @@ class HomeworkDetailHandler extends Handler {
|
||||
this.response.body = {
|
||||
tdoc, tsdoc, udict, ddocs, page, dpcount, dcount,
|
||||
};
|
||||
this.response.body.tdoc.content = this.response.body.tdoc.content
|
||||
.replace(/\(file:\/\//g, `(./${tdoc.docId}/file/`)
|
||||
.replace(/="file:\/\//g, `="./${tdoc.docId}/file/`);
|
||||
if (
|
||||
(contest.isNotStarted(tdoc) || (!tsdoc?.attend && !contest.isDone(tdoc)))
|
||||
&& !this.user.own(tdoc)
|
||||
@@ -226,11 +236,89 @@ class HomeworkEditHandler extends Handler {
|
||||
async postDelete(domainId: string, tid: ObjectId) {
|
||||
const tdoc = await contest.get(domainId, tid);
|
||||
if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_HOMEWORK);
|
||||
await contest.del(domainId, tid);
|
||||
await Promise.all([
|
||||
record.updateMulti(domainId, { domainId, contest: tid }, undefined, undefined, { contest: '' }),
|
||||
contest.del(domainId, tid),
|
||||
storage.del(tdoc.files?.map((i) => `contest/${domainId}/${tid}/${i.name}`) || [], this.user._id),
|
||||
]);
|
||||
this.response.redirect = this.url('homework_main');
|
||||
}
|
||||
}
|
||||
|
||||
export class HomeworkFilesHandler extends Handler {
|
||||
tdoc: Tdoc;
|
||||
|
||||
@param('tid', Types.ObjectId)
|
||||
async prepare(domainId: string, tid: ObjectId) {
|
||||
this.tdoc = await contest.get(domainId, tid);
|
||||
if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_HOMEWORK);
|
||||
else this.checkPerm(PERM.PERM_EDIT_HOMEWORK_SELF);
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectId)
|
||||
async get(domainId: string, tid: ObjectId) {
|
||||
if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_HOMEWORK);
|
||||
this.response.body = {
|
||||
tdoc: this.tdoc,
|
||||
tsdoc: await contest.getStatus(domainId, this.tdoc.docId, this.user._id),
|
||||
udoc: await user.getById(domainId, this.tdoc.owner),
|
||||
files: sortFiles(this.tdoc.files || []),
|
||||
urlForFile: (filename: string) => this.url('homework_file_download', { tid, filename }),
|
||||
};
|
||||
this.response.pjax = 'partials/files.html';
|
||||
this.response.template = 'homework_files.html';
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectId)
|
||||
@post('filename', Types.Filename, true)
|
||||
async postUploadFile(domainId: string, tid: ObjectId, filename: string) {
|
||||
if ((this.tdoc.files?.length || 0) >= system.get('limit.contest_files')) {
|
||||
throw new FileLimitExceededError('count');
|
||||
}
|
||||
const file = this.request.files?.file;
|
||||
if (!file) throw new ValidationError('file');
|
||||
const f = statSync(file.filepath);
|
||||
const size = Math.sum((this.tdoc.files || []).map((i) => i.size)) + f.size;
|
||||
if (size >= system.get('limit.contest_files_size')) {
|
||||
throw new FileLimitExceededError('size');
|
||||
}
|
||||
await storage.put(`contest/${domainId}/${tid}/${filename}`, file.filepath, this.user._id);
|
||||
const meta = await storage.getMeta(`contest/${domainId}/${tid}/${filename}`);
|
||||
const payload = { _id: filename, name: filename, ...pick(meta, ['size', 'lastModified', 'etag']) };
|
||||
if (!meta) throw new FileUploadError();
|
||||
await contest.edit(domainId, tid, { files: [...(this.tdoc.files || []), payload] });
|
||||
this.back();
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectId)
|
||||
@post('files', Types.ArrayOf(Types.Filename))
|
||||
async postDeleteFiles(domainId: string, tid: ObjectId, files: string[]) {
|
||||
await Promise.all([
|
||||
storage.del(files.map((t) => `contest/${domainId}/${tid}/${t}`), this.user._id),
|
||||
contest.edit(domainId, tid, { files: this.tdoc.files.filter((i) => !files.includes(i.name)) }),
|
||||
]);
|
||||
this.back();
|
||||
}
|
||||
}
|
||||
|
||||
export class HomeworkFileDownloadHandler extends Handler {
|
||||
@param('tid', Types.ObjectId)
|
||||
@param('filename', Types.Filename)
|
||||
@param('noDisposition', Types.Boolean)
|
||||
async get(domainId: string, tid: ObjectId, filename: string, noDisposition = false) {
|
||||
this.response.addHeader('Cache-Control', 'public');
|
||||
const target = `contest/${domainId}/${tid}/${filename}`;
|
||||
const file = await storage.getMeta(target);
|
||||
await oplog.log(this, 'download.file.contest', {
|
||||
target,
|
||||
size: file?.size || 0,
|
||||
});
|
||||
this.response.redirect = await storage.signDownloadLink(
|
||||
target, noDisposition ? undefined : filename, false, 'user',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function apply(ctx) {
|
||||
ctx.Route('homework_main', '/homework', HomeworkMainHandler, PERM.PERM_VIEW_HOMEWORK);
|
||||
ctx.Route('homework_create', '/homework/create', HomeworkEditHandler);
|
||||
@@ -242,4 +330,6 @@ export async function apply(ctx) {
|
||||
);
|
||||
ctx.Route('homework_code', '/homework/:tid/code', ContestCodeHandler, PERM.PERM_VIEW_HOMEWORK);
|
||||
ctx.Route('homework_edit', '/homework/:tid/edit', HomeworkEditHandler);
|
||||
ctx.Route('homework_files', '/homework/:tid/file', HomeworkFilesHandler, PERM.PERM_VIEW_HOMEWORK);
|
||||
ctx.Route('homework_file_download', '/homework/:tid/file/:filename', HomeworkFileDownloadHandler, PERM.PERM_VIEW_HOMEWORK);
|
||||
}
|
||||
@@ -173,7 +173,10 @@ class TrainingDetailHandler extends Handler {
|
||||
async postDelete(domainId: string, tid: ObjectId) {
|
||||
const tdoc = await training.get(domainId, tid);
|
||||
if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_TRAINING);
|
||||
await training.del(domainId, tid);
|
||||
await Promise.all([
|
||||
training.del(domainId, tid),
|
||||
storage.del(tdoc.files?.map((i) => `training/${domainId}/${tid}/${i.name}`) || [], this.user._id),
|
||||
]);
|
||||
this.response.redirect = this.url('training_main');
|
||||
}
|
||||
}
|
||||
@@ -275,7 +278,7 @@ export class TrainingFilesHandler extends Handler {
|
||||
@post('files', Types.ArrayOf(Types.Filename))
|
||||
async postDeleteFiles(domainId: string, tid: ObjectId, files: string[]) {
|
||||
await Promise.all([
|
||||
storage.del(files.map((t) => `contest/${domainId}/${tid}/${t}`), this.user._id),
|
||||
storage.del(files.map((t) => `training/${domainId}/${tid}/${t}`), this.user._id),
|
||||
training.edit(domainId, tid, { files: this.tdoc.files.filter((i) => !files.includes(i.name)) }),
|
||||
]);
|
||||
this.back();
|
||||
|
||||
@@ -760,7 +760,6 @@ export async function del(domainId: string, tid: ObjectId) {
|
||||
await Promise.all([
|
||||
document.deleteOne(domainId, document.TYPE_CONTEST, tid),
|
||||
document.deleteMultiStatus(domainId, document.TYPE_CONTEST, { docId: tid }),
|
||||
document.deleteMulti(domainId, document.TYPE_DISCUSSION, { parentType: document.TYPE_CONTEST, parentId: tid }),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import ScheduleModel from './model/schedule';
|
||||
import StorageModel from './model/storage';
|
||||
import * as system from './model/system';
|
||||
import TaskModel from './model/task';
|
||||
import * as training from './model/training';
|
||||
import user from './model/user';
|
||||
import {
|
||||
iterateAllContest,
|
||||
@@ -639,6 +640,23 @@ const scripts: UpgradeScript[] = [
|
||||
await domain.setRoles(ddoc._id, ddoc.roles);
|
||||
});
|
||||
},
|
||||
async function _86_87() {
|
||||
logger.info('Removing unused files...');
|
||||
return await iterateAllDomain(async ({ _id }) => {
|
||||
logger.info('Processing domain %s', _id);
|
||||
const contestFilesList = await StorageModel.list(`contest/${_id}`);
|
||||
const trainingFilesList = await StorageModel.list(`training/${_id}`);
|
||||
const tdocs = await contest.getMulti(_id, {}).toArray();
|
||||
const trdocs = await training.getMulti(_id, {}).toArray();
|
||||
let existsFiles = [];
|
||||
for (const tdoc of tdocs) existsFiles = existsFiles.concat((tdoc.files || []).map((i) => `contest/${_id}/${tdoc.docId}/${i.name}`));
|
||||
await StorageModel.del(contestFilesList.filter((i) => !existsFiles.includes(i.name)).map((i) => i.name));
|
||||
existsFiles = [];
|
||||
for (const tdoc of trdocs) existsFiles = existsFiles.concat((tdoc.files || []).map((i) => `training/${_id}/${tdoc.docId}/${i.name}`));
|
||||
await StorageModel.del(trainingFilesList.filter((i) => !existsFiles.includes(i.name)).map((i) => i.name));
|
||||
logger.info('Domain %s done', _id);
|
||||
});
|
||||
},
|
||||
];
|
||||
|
||||
export default scripts;
|
||||
@@ -39,7 +39,7 @@ const page = new NamedPage(['contest_edit', 'contest_create', 'homework_create',
|
||||
window.location.href = res.url;
|
||||
});
|
||||
}
|
||||
const message = `Confirm deleting this ${pagename.split('_')[0]}? Its status will be deleted as well.`;
|
||||
const message = `Confirm deleting this ${pagename.split('_')[0]}? Its files and status will be deleted as well.`;
|
||||
return new ConfirmDialog({
|
||||
$body: tpl.typoMsg(i18n(message)),
|
||||
}).open().then((action) => {
|
||||
|
||||
@@ -92,7 +92,7 @@ function handleDrop(ev) {
|
||||
handleClickUpload(files);
|
||||
}
|
||||
|
||||
const page = new NamedPage(['home_files', 'contest_manage', 'training_files'], () => {
|
||||
const page = new NamedPage(['home_files', 'contest_manage', 'training_files', 'homework_files'], () => {
|
||||
$(document).on('click', '[name="upload_file"]', () => handleClickUpload());
|
||||
$(document).on('click', '[name="remove_selected"]', () => handleClickRemoveSelected());
|
||||
$(document).on('dragover', '.files', (ev) => handleDragOver(ev));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.page--home_files, .page--training_files, .page--contest_detail, .page--contest_manage
|
||||
.page--home_files, .page--training_files, .page--contest_detail, .page--contest_manage, .page--homework_files
|
||||
.col--checkbox
|
||||
width: 60px
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import $ from 'jquery';
|
||||
import { ConfirmDialog } from 'vj/components/dialog';
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import { i18n, request, tpl } from 'vj/utils';
|
||||
|
||||
export default new NamedPage('homework_edit', () => {
|
||||
let confirmed = false;
|
||||
$(document).on('click', '[value="delete"]', (ev) => {
|
||||
ev.preventDefault();
|
||||
if (confirmed) {
|
||||
return request.post('', { operation: 'delete' }).then((res) => {
|
||||
window.location.href = res.url;
|
||||
});
|
||||
}
|
||||
const message = 'Confirm deleting this homework? Its files and status will be deleted as well.';
|
||||
return new ConfirmDialog({
|
||||
$body: tpl.typoMsg(i18n(message)),
|
||||
}).open().then((action) => {
|
||||
if (action !== 'yes') return;
|
||||
confirmed = true;
|
||||
ev.target.click();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,7 @@ export default new NamedPage('training_edit', () => {
|
||||
window.location.href = res.url;
|
||||
});
|
||||
}
|
||||
const message = 'Confirm deleting this training? Its status will be deleted as well.';
|
||||
const message = 'Confirm deleting this training? Its files and status will be deleted as well.';
|
||||
return new ConfirmDialog({
|
||||
$body: tpl.typoMsg(i18n(message)),
|
||||
}).open().then((action) => {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends "layout/basic.html" %}
|
||||
{% block content %}
|
||||
{{ set(UiContext, 'tdoc', tdoc) }}
|
||||
<div class="row">
|
||||
<div class="medium-9 columns">
|
||||
<div class="section">
|
||||
<div class="section__header">
|
||||
<h1 class="section__title">{{ _('Files') }}</h1>
|
||||
<div class="section__tools">
|
||||
<button class="primary rounded button" name="upload_file">{{ _('Upload File') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
{{ noscript_note.render() }}
|
||||
{% include "partials/files.html" %}
|
||||
<div class="section__body">
|
||||
<button class="rounded button" name="remove_selected">{{ _('Remove Selected') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="medium-3 columns">
|
||||
{% include "partials/homework_sidebar.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -55,6 +55,9 @@
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('homework_edit', tid=tdoc.docId) }}">
|
||||
<span class="icon icon-edit"></span> {{ _('Edit Homework') }}
|
||||
</a></li>
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('homework_files', tid=tdoc.docId) }}">
|
||||
<span class="icon icon-file"></span> {{ _('Files') }}
|
||||
</a></li>
|
||||
{% endif %}
|
||||
{% if handler.user.own(tdoc) or handler.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE) or handler.user.hasPerm(perm.PERM_READ_RECORD_CODE) %}
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('homework_code', tid=tdoc.docId) }}" data-no-instant>
|
||||
|
||||
Reference in new issue
Block a user