add training files

pull/472/head
panda 2 years ago
parent 3600aff598
commit dc0e397d77

@ -552,7 +552,7 @@ export class ContestFilesHandler extends ContestDetailBaseHandler {
if (filename.includes('/') || filename.includes('..')) throw new ValidationError('filename', null, 'Bad filename');
await storage.put(`contest/${domainId}/${tid}/${filename}`, file.filepath, this.user._id);
const meta = await storage.getMeta(`contest/${domainId}/${tid}/${filename}`);
const payload = { name: filename, ...pick(meta, ['size', 'lastModified', 'etag']) };
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();

@ -1,15 +1,24 @@
import assert from 'assert';
import { statSync } from 'fs-extra';
import { pick } from 'lodash';
import { FilterQuery, ObjectID } from 'mongodb';
import { ProblemNotFoundError, ValidationError } from '../error';
import { sortFiles } from '@hydrooj/utils/lib/utils';
import {
FileLimitExceededError, FileUploadError, ProblemNotFoundError, ValidationError,
} from '../error';
import { Tdoc, TrainingDoc } from '../interface';
import paginate from '../lib/paginate';
import { PERM, PRIV, STATUS } from '../model/builtin';
import * as oplog from '../model/oplog';
import problem from '../model/problem';
import storage from '../model/storage';
import * as system from '../model/system';
import * as training from '../model/training';
import user from '../model/user';
import * as bus from '../service/bus';
import { Handler, param, Types } from '../service/server';
import {
Handler, param, post, Types,
} from '../service/server';
async function _parseDagJson(domainId: string, _dag: string): Promise<Tdoc['dag']> {
const parsed = [];
@ -149,6 +158,9 @@ class TrainingDetailHandler extends Handler {
this.response.body = {
tdoc, tsdoc, pids, pdict, psdict, ndict, nsdict, udoc, udict, selfPsdict,
};
this.response.body.tdoc.description = this.response.body.tdoc.description
.replace(/\(file:\/\//g, `(./${tdoc.docId}/file/`)
.replace(/="file:\/\//g, `="./${tdoc.docId}/file/`);
this.response.pjax = 'partials/training_detail.html';
this.response.template = 'training_detail.html';
}
@ -219,9 +231,86 @@ class TrainingEditHandler extends Handler {
}
}
export class TrainingFilesHandler extends Handler {
tdoc: TrainingDoc;
@param('tid', Types.ObjectID)
async prepare(domainId: string, tid: ObjectID) {
this.tdoc = await training.get(domainId, tid);
if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_TRAINING);
else this.checkPerm(PERM.PERM_EDIT_TRAINING_SELF);
}
@param('tid', Types.ObjectID)
async get(domainId: string, tid: ObjectID) {
if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_TRAINING);
this.response.body = {
tdoc: this.tdoc,
tsdoc: await training.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('training_file_download', { tid, filename }),
};
this.response.pjax = 'partials/files.html';
this.response.template = 'training_files.html';
}
@param('tid', Types.ObjectID)
@post('filename', Types.Name, true)
async postUploadFile(domainId: string, tid: ObjectID, filename: string) {
if ((this.tdoc.files?.length || 0) >= system.get('limit.training_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.training_files_size')) {
throw new FileLimitExceededError('size');
}
if (!filename) filename = file.originalFilename || String.random(16);
if (filename.includes('/') || filename.includes('..')) throw new ValidationError('filename', null, 'Bad filename');
await storage.put(`training/${domainId}/${tid}/${filename}`, file.filepath, this.user._id);
const meta = await storage.getMeta(`training/${domainId}/${tid}/${filename}`);
const payload = { _id: filename, name: filename, ...pick(meta, ['size', 'lastModified', 'etag']) };
if (!meta) throw new FileUploadError();
await training.edit(domainId, tid, { files: [...(this.tdoc.files || []), payload] });
this.back();
}
@param('tid', Types.ObjectID)
@post('files', Types.Array)
async postDeleteFiles(domainId: string, tid: ObjectID, files: string[]) {
await Promise.all([
storage.del(files.map((t) => `contest/${domainId}/${tid}/${t}`), this.user._id),
training.edit(domainId, tid, { files: this.tdoc.files.filter((i) => !files.includes(i.name)) }),
]);
this.back();
}
}
export class TrainingFileDownloadHandler extends Handler {
@param('tid', Types.ObjectID)
@param('filename', Types.Name)
@param('noDisposition', Types.Boolean)
async get(domainId: string, tid: ObjectID, filename: string, noDisposition = false) {
this.response.addHeader('Cache-Control', 'public');
const target = `training/${domainId}/${tid}/${filename}`;
const file = await storage.getMeta(target);
await oplog.log(this, 'download.file.training', {
target,
size: file?.size || 0,
});
this.response.redirect = await storage.signDownloadLink(
target, noDisposition ? undefined : filename, false, 'user',
);
}
}
export async function apply(ctx) {
ctx.Route('training_main', '/training', TrainingMainHandler, PERM.PERM_VIEW_TRAINING);
ctx.Route('training_create', '/training/create', TrainingEditHandler);
ctx.Route('training_detail', '/training/:tid', TrainingDetailHandler, PERM.PERM_VIEW_TRAINING);
ctx.Route('training_edit', '/training/:tid/edit', TrainingEditHandler);
ctx.Route('training_files', '/training/:tid/file', TrainingFilesHandler, PERM.PERM_VIEW_TRAINING);
ctx.Route('training_file_download', '/training/:tid/file/:filename', TrainingFileDownloadHandler, PERM.PERM_VIEW_TRAINING);
}

@ -367,6 +367,8 @@ export interface Tdoc<docType = document['TYPE_CONTEST'] | document['TYPE_TRAINI
lockAt?: Date;
unlocked?: boolean;
access?: number;
autoHide?: boolean;
unrankUsers?: number[];
/**
* In hours
@ -495,6 +497,7 @@ export interface OplogDoc extends Record<string, any> {
export interface ContestStat extends Record<string, any> {
detail: Record<number, Record<string, any>>,
unrank?: boolean,
}
export interface ContestRule<T = any> {

@ -540,7 +540,7 @@ export async function add(
return res;
}
export async function edit(domainId: string, tid: ObjectID, $set: any) {
export async function edit(domainId: string, tid: ObjectID, $set: Partial<Tdoc>) {
if ($set.rule && !RULES[$set.rule]) throw new ValidationError('rule');
const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid);
if (!tdoc) throw new ContestNotFoundError(domainId, tid);

@ -215,6 +215,8 @@ SystemSetting(
Setting('setting_limits', 'limit.user_files_size', 128 * 1024 * 1024, 'number', 'limit.user_files_size', 'Max total file size for user'),
Setting('setting_limits', 'limit.contest_files', 100, 'number', 'limit.contest_files', 'Max files for contest'),
Setting('setting_limits', 'limit.contest_files_size', 128 * 1024 * 1024, 'number', 'limit.contest_files_size', 'Max total file size for contest'),
Setting('setting_limits', 'limit.training_files', 100, 'number', 'limit.training_files', 'Max files for training'),
Setting('setting_limits', 'limit.training_files_size', 128 * 1024 * 1024, 'number', 'limit.training_files_size', 'Max total file size for training'),
Setting('setting_limits', 'limit.submission', 60, 'number', 'limit.submission', 'Max submission count per minute'),
Setting('setting_limits', 'limit.submission_user', 15, 'number', 'limit.submission_user', 'Max submission count per user per minute'),
Setting('setting_limits', 'limit.pretest', 60, 'number', 'limit.pretest', 'Max pretest count per minute'),

@ -927,6 +927,7 @@ View problem solutions: 查看题解
View Problem: 查看题目
View problems: 查看题目
View this domain: 查看此域
View Training: 查看训练计划
View training plans: 查看训练计划
View: 查看
Visible to registered users: 对注册用户可见

@ -92,7 +92,7 @@ function handleDrop(ev) {
handleClickUpload(files);
}
const page = new NamedPage(['home_files', 'contest_files'], () => {
const page = new NamedPage(['home_files', 'contest_files', 'training_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--contest_files
.page--home_files,.page--contest_files,.page--training_files
.col--checkbox
width: 60px

@ -1,7 +1,4 @@
{% extends "layout/basic.html" %}
{% import "components/contest.html" as contest with context %}
{% import "components/record.html" as record with context %}
{% import "components/problem.html" as problem with context %}
{% block content %}
{{ set(UiContext, 'tdoc', tdoc) }}
<div class="row">

@ -75,6 +75,9 @@
<li class="menu__item"><a class="menu__link" href="{{ url('training_edit', tid=tdoc.docId) }}">
<span class="icon icon-edit"></span> {{ _('Edit') }}
</a></li>
<li class="menu__item"><a class="menu__link" href="{{ url('training_files', tid=tdoc.docId) }}">
<span class="icon icon-file"></span> {{ _('Files') }}
</a></li>
{% endif %}
<li class="menu__item"><a class="menu__link" href="{{ url('wiki_help', anchor='training') }}">
<span class="icon icon-help"></span> {{ _('Help') }}

@ -0,0 +1,65 @@
{% 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">
<div class="section side">
<div>
<ol class="menu">
<li class="menu__item"><a class="menu__link" href="{{ url('training_detail', tid=tdoc.docId) }}">
<span class="icon icon-award"></span> {{ _('View Training') }}
</a></li>
{% if not tsdoc['enroll'] and handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
<li class="menu__item">
<form action="" method="POST">
<input type="hidden" name="operation" value="enroll">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button class="menu__link" type="submit">
<span class="icon icon-add"></span> {{ _('Enroll Training') }}
</button>
</form>
</li>
{% endif %}
{% if handler.user.own(tdoc) or handler.user.hasPerm(perm.PERM_EDIT_TRAINING) %}
<li class="menu__item"><a class="menu__link" href="{{ url('training_edit', tid=tdoc.docId) }}">
<span class="icon icon-edit"></span> {{ _('Edit') }}
</a></li>
<li class="menu__item"><a class="menu__link" href="{{ url('training_files', tid=tdoc.docId) }}">
<span class="icon icon-file"></span> {{ _('Files') }}
</a></li>
{% endif %}
<li class="menu__item"><a class="menu__link" href="{{ url('wiki_help', anchor='training') }}">
<span class="icon icon-help"></span> {{ _('Help') }}
</a></li>
<li class="menu__seperator"></li>
</ol>
</div>
<div class="section__body typo">
<dl class="large horizontal">
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
<dt>{{ _('Status') }}</dt><dd>{% if tsdoc['enroll'] %}{{ _('Completed' if tsdoc['done'] else 'In Progress') }}{% else %}{{ _('Not Enrolled') }}{% endif %}</dd>
{% endif %}
<dt>{{ _('Enrollees') }}</dt><dd>{{ tdoc.attend|default(0) }}</dd>
<dt>{{ _('Created By') }}</dt>
<dd>{{ user.render_inline(udoc, badge=false) }}</dd>
</dl>
</div>
</div>
</div>
</div>
{% endblock %}
Loading…
Cancel
Save