Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions framework/framework/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,12 @@ export default (logger, xff, xhost) => async (ctx: KoaContext, next: Next) => {
}
if (!response.type) {
if (response.pjax && args.pjax) {
const html = await handler.renderHTML(response.pjax, response.body);
response.body = { fragments: [{ html }] };
const pjax = typeof response.pjax === 'string' ? [[response.pjax, {}]] : response.pjax;
response.body = {
fragments: (await Promise.all(
pjax.map(async ([template, extra]) => handler.renderHTML(template, { ...response.body, ...extra })),
)).map((i) => ({ html: i })),
};
response.type = 'application/json';
} else if (
request.json || response.redirect
Expand Down
2 changes: 1 addition & 1 deletion framework/framework/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export interface HydroResponse {
* If set, and pjax content was request from client,
* The template will be used for rendering.
*/
pjax?: string;
pjax?: string | (readonly [string, Record<string, any>])[];
redirect?: string;
disposition?: string;
etag?: string;
Expand Down
1 change: 0 additions & 1 deletion packages/hydrooj/locales/ko.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,6 @@ name: 이름
Name: 이름
Network error: Network error
Never expire: 영원히 만료되지 않음
New dataset: 새 데이터셋
New Email: 새 이메일
New file: 새 파일
New Password: 새 비밀번호
Expand Down
12 changes: 7 additions & 5 deletions packages/hydrooj/locales/zh.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,14 @@ Contact Us: 联系我们
Contact: 联系
content: 内容
Content: 内容
Contest {0} has a new clarification about {1}, please go to contest management to reply.: 比赛 {0} 有一条关于 {1} 的新问答,请前往比赛管理页面回复。
Contest {0} jury replied to your clarification, please go to contest page to view.: 比赛 {0} 的裁判回复了您的问答,请前往比赛页面查看。
Contest Clarifications: 问答
Contest {0} has a new clarification about {1}, please go to contest management to reply.: 比赛 {0} 有一条关于 {1} 的新答疑,请前往比赛管理页面回复。
Contest {0} jury replied to your clarification, please go to contest page to view.: 比赛 {0} 的裁判回复了您的答疑,请前往比赛页面查看。
Contest Clarifications: 赛时答疑
Contest Management: 比赛管理
Contest scoreboard is not visible.: 当前比赛成绩表隐藏,暂不可显示。
Contest Scoreboard: 比赛成绩表
contest_balloon: 气球状态
contest_clarification: 赛时答疑
contest_create: 创建比赛
contest_detail_problem_submit: 递交比赛题目
contest_detail_problem: 题目详情
Expand Down Expand Up @@ -476,6 +477,7 @@ Manage: 管理
Manager: 管理
Mark Green: 标绿
Mark Yellow: 标黄
Materials: 材料
Max files for user: 单用户最大文件数量
Max files per problem: 单题最大文件数量
Max files size per problem: 单题最大总文件大小
Expand All @@ -501,7 +503,6 @@ name: 名称
Name: 名称
Network error: 网络错误
Never expire: 从不过期
New dataset: 新测试数据
New Email: 新电子邮件
New file: 新文件
New Password: 新密码
Expand Down Expand Up @@ -532,10 +533,11 @@ Oh, the user doesn't have any contributions!: 啊哦,这个用户还没贡献
Oh, the user hasn't created any discussions yet!: 这个用户还没有发布过讨论
Oh, the user hasn't submitted yet!: 这个用户还没有交过题 _(:зゝ∠)_
Oh, there are no tasks that match the filter!: 喔,目前没有符合过滤条件的任务。
Oh, there is no clarification!: 喔,目前没有提问
Oh, there is no clarification!: 喔,目前没有答疑
Oh, there is no task in the queue!: 喔,队列中目前没有任务。
Ok: 确定
Only A-Z, a-z, 0-9 and _ are accepted: 只接受 A-Z, a-z, 0-9 和 _
Only Partic.: 仅参赛者
Oops, there are no results.: 呀,没有结果。
Oops!: Oops!
Open in Problem Set: 在题库中打开
Expand Down
157 changes: 100 additions & 57 deletions packages/hydrooj/src/handler/contest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError,
InvalidTokenError, NotAssignedError, NotFoundError, PermissionError, ValidationError,
} from '../error';
import { ScoreboardConfig, Tdoc } from '../interface';
import { FileInfo, ScoreboardConfig, Tdoc } from '../interface';
import { PERM, PRIV, STATUS } from '../model/builtin';
import * as contest from '../model/contest';
import * as discussion from '../model/discussion';
Expand All @@ -25,7 +25,6 @@ import problem from '../model/problem';
import record from '../model/record';
import ScheduleModel from '../model/schedule';
import storage from '../model/storage';
import * as system from '../model/system';
import user from '../model/user';
import {
Handler, param, post, Type, Types,
Expand Down Expand Up @@ -155,13 +154,13 @@ export class ContestDetailHandler extends ContestDetailBaseHandler {
tdoc: this.tdoc,
tsdoc: this.tsdocAsPublic(),
udict,
files: sortFiles(this.tdoc.files || []),
urlForFile: (filename: string) => this.url('contest_file_download', { tid, filename }),
files: (this.tsdoc?.attend && !contest.isNotStarted(this.tdoc)) ? sortFiles(this.tdoc.privateFiles || []) : [],
urlForFile: (filename: string) => this.url('contest_file_download', { tid, filename, type: 'private' }),
};
if (this.request.json) return;
this.response.body.tdoc.content = this.response.body.tdoc.content
.replace(/\(file:\/\//g, `(./${this.tdoc.docId}/file/`)
.replace(/="file:\/\//g, `="./${this.tdoc.docId}/file/`);
.replace(/\(file:\/\//g, `(./${this.tdoc.docId}/file/public/`)
.replace(/="file:\/\//g, `="./${this.tdoc.docId}/file/public/`);
}

@param('tid', Types.ObjectId)
Expand Down Expand Up @@ -358,6 +357,8 @@ export class ContestEditHandler extends Handler {
pids: tid ? this.tdoc.pids.join(',') : '',
beginAt,
page_name: tid ? 'contest_edit' : 'contest_create',
files: tid ? this.tdoc.files : [],
urlForFile: (filename: string) => this.url('contest_file_download', { tid, filename, type: 'public' }),
};
}

Expand Down Expand Up @@ -443,7 +444,11 @@ 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),
storage.del(
(this.tdoc.files?.map((i) => `contest/${domainId}/${tid}/public/${i.name}`) || [])
.concat(this.tdoc.privateFiles?.map((i) => `contest/${domainId}/${tid}/private/${i.name}`) || []),
this.user._id,
),
]));
this.response.redirect = this.url('contest_main');
}
Expand Down Expand Up @@ -506,22 +511,95 @@ export class ContestCodeHandler extends Handler {
export class ContestManagementHandler extends ContestManagementBaseHandler {
@param('tid', Types.ObjectId)
async get(domainId: string, tid: ObjectId) {
const tcdocs = await contest.getMultiClarification(domainId, tid);
this.response.body = {
tdoc: this.tdoc,
tsdoc: this.tsdoc,
owner_udoc: await user.getById(domainId, this.tdoc.owner),
pdict: await problem.getList(domainId, this.tdoc.pids, true, true, [...problem.PROJECTION_CONTEST_LIST, 'tag']),
files: sortFiles(this.tdoc.files || []),
privateFiles: sortFiles(this.tdoc.privateFiles || []),
urlForFile: (filename: string, type: string) => this.url('contest_file_download', { tid, filename, type }),
};
this.response.pjax = [
['partials/files.html', { filetype: 'public' }],
['partials/files.html', {
files: this.response.body.privateFiles,
filetype: 'private',
}],
];
this.response.template = 'contest_manage.html';
}

@param('tid', Types.ObjectId)
@post('filename', Types.Filename, true)
@post('type', Types.Range(['private', 'public']), true)
async postUploadFile(domainId: string, tid: ObjectId, filename: string, type: 'private' | 'public' = 'private') {
const allFiles = [...(this.tdoc.files || []), ...(this.tdoc.privateFiles || [])];
if (allFiles.length >= this.ctx.setting.get('limit.contest_files')) {
throw new FileLimitExceededError('count');
}
const file = this.request.files?.file;
if (!file) throw new ValidationError('file');
if (Math.sum(allFiles.map((i) => i.size)) + file.size >= this.ctx.setting.get('limit.contest_files_size')) {
throw new FileLimitExceededError('size');
}
Comment thread
undefined-moe marked this conversation as resolved.
filename ||= file.originalFilename || randomstring(16);
const target = `contest/${domainId}/${tid}/${type}/${filename}`;
await storage.put(target, file.filepath, this.user._id);
const meta = await storage.getMeta(target);
const payload = { _id: filename, name: filename, ...pick(meta, ['size', 'lastModified', 'etag']) };
if (!meta) throw new FileUploadError();
const updateList = (files: FileInfo[], newFile: FileInfo) => (files || []).filter((i) => i._id !== newFile._id).concat(newFile);
await contest.edit(domainId, tid, {
files: type === 'private' ? this.tdoc.files : updateList(this.tdoc.files, payload),
privateFiles: type === 'private' ? updateList(this.tdoc.privateFiles, payload) : this.tdoc.privateFiles,
});
this.back();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@param('tid', Types.ObjectId)
@post('files', Types.ArrayOf(Types.Filename))
@post('type', Types.Range(['public', 'private']), true)
async postDeleteFiles(domainId: string, tid: ObjectId, files: string[], type = 'private') {
await Promise.all([
storage.del(files.map((t) => `contest/${domainId}/${tid}/${type}/${t}`), this.user._id),
contest.edit(domainId, tid, type === 'private'
? { privateFiles: this.tdoc.privateFiles.filter((i) => !files.includes(i.name)) }
: { files: this.tdoc.files.filter((i) => !files.includes(i.name)) },
Comment thread
undefined-moe marked this conversation as resolved.
Outdated
),
]);
this.back();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@param('pid', Types.PositiveInt)
@param('score', Types.PositiveInt)
async postSetScore(domainId: string, pid: number, score: number) {
if (!this.tdoc.pids.includes(pid)) throw new ValidationError('pid');
this.tdoc.score ||= {};
this.tdoc.score[pid] = score;
await contest.edit(domainId, this.tdoc.docId, { score: this.tdoc.score });
await contest.recalcStatus(domainId, this.tdoc.docId);
this.back();
}
}

class ContestClarificationHandler extends ContestManagementBaseHandler {
@param('tid', Types.ObjectId)
async get(domainId: string, tid: ObjectId) {
const tcdocs = await contest.getMultiClarification(domainId, tid);
this.response.body = {
tdoc: this.tdoc,
tsdoc: this.tsdoc,
owner_udoc: await user.getById(domainId, this.tdoc.owner),
pdict: await problem.getList(domainId, this.tdoc.pids, true, true, [...problem.PROJECTION_CONTEST_LIST, 'tag']),
tcdocs,
udict: await user.getListForRender(
domainId, tcdocs.map((i) => i.owner),
this.user.hasPerm(PERM.PERM_VIEW_DISPLAYNAME) ? ['displayName'] : [],
),
tcdocs,
urlForFile: (filename: string) => this.url('contest_file_download', { tid, filename }),
};
this.response.pjax = 'partials/files.html';
this.response.template = 'contest_manage.html';
this.response.pjax = 'partials/contest_clarification.html';
this.response.template = 'contest_clarification.html';
}

@param('tid', Types.ObjectId)
Expand Down Expand Up @@ -554,57 +632,21 @@ export class ContestManagementHandler extends ContestManagementBaseHandler {
}
this.back();
}

@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 size = Math.sum((this.tdoc.files || []).map((i) => i.size)) + file.size;
if (size >= system.get('limit.contest_files_size')) {
throw new FileLimitExceededError('size');
}
filename ||= file.originalFilename || randomstring(16);
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();
}

@param('pid', Types.PositiveInt)
@param('score', Types.PositiveInt)
async postSetScore(domainId: string, pid: number, score: number) {
if (!this.tdoc.pids.includes(pid)) throw new ValidationError('pid');
this.tdoc.score ||= {};
this.tdoc.score[pid] = score;
await contest.edit(domainId, this.tdoc.docId, { score: this.tdoc.score });
await contest.recalcStatus(domainId, this.tdoc.docId);
this.back();
}
}

export class ContestFileDownloadHandler extends ContestDetailBaseHandler {
@param('tid', Types.ObjectId)
@param('filename', Types.Filename)
@param('noDisposition', Types.Boolean)
async get(domainId: string, tid: ObjectId, filename: string, noDisposition = false) {
@param('type', Types.Range(['public', 'private']), true)
async get(domainId: string, tid: ObjectId, filename: string, noDisposition = false, type = 'private') {
if (type === 'private' && !this.user.own(this.tdoc)) {
if (!this.tsdoc?.attend) throw new ContestNotAttendedError(domainId, tid);
if (!contest.isOngoing(this.tdoc) && !contest.isDone(this.tdoc)) throw new ContestNotLiveError(domainId, tid);
if (!this.tsdoc.startAt) await contest.setStatus(domainId, tid, this.user._id, { startAt: new Date() });
}
this.response.addHeader('Cache-Control', 'public');
const target = `contest/${domainId}/${tid}/${filename}`;
const target = `contest/${domainId}/${tid}/${type}/${filename}`;
const file = await storage.getMeta(target);
await oplog.log(this, 'download.file.contest', {
target,
Expand Down Expand Up @@ -809,8 +851,9 @@ export async function apply(ctx: Context) {
ctx.Route('contest_edit', '/contest/:tid/edit', ContestEditHandler, PERM.PERM_VIEW_CONTEST);
ctx.Route('contest_print', '/contest/:tid/print', ContestPrintHandler, PERM.PERM_VIEW_CONTEST);
ctx.Route('contest_manage', '/contest/:tid/management', ContestManagementHandler);
ctx.Route('contest_clarification', '/contest/:tid/clarification', ContestClarificationHandler);
ctx.Route('contest_code', '/contest/:tid/code', ContestCodeHandler, PERM.PERM_VIEW_CONTEST);
ctx.Route('contest_file_download', '/contest/:tid/file/:filename', ContestFileDownloadHandler, PERM.PERM_VIEW_CONTEST);
ctx.Route('contest_file_download', '/contest/:tid/file/:type/:filename', ContestFileDownloadHandler, PERM.PERM_VIEW_CONTEST);
ctx.Route('contest_user', '/contest/:tid/user', ContestUserHandler, PERM.PERM_VIEW_CONTEST);
ctx.Route('contest_balloon', '/contest/:tid/balloon', ContestBalloonHandler, PERM.PERM_VIEW_CONTEST);
ctx.worker.addHandler('contest', async (doc) => {
Expand Down
25 changes: 6 additions & 19 deletions packages/hydrooj/src/handler/problem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -646,28 +646,15 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
notUsage = true;

@param('d', Types.CommaSeperatedArray, true)
@param('pjax', Types.Boolean)
@param('sidebar', Types.Boolean)
async get(domainId: string, d = ['testdata', 'additional_file'], pjax = false, sidebar = false) {
async get({ }, d = ['testdata', 'additional_file'], sidebar = false) {
Comment thread
undefined-moe marked this conversation as resolved.
if (this.tdoc) throw new ContestNotEndedError();
this.response.body.testdata = d.includes('testdata') ? sortFiles(this.pdoc.data || []) : [];
this.response.body.testdata = sortFiles(this.pdoc.data || []);
this.response.body.additional_file = sortFiles(this.pdoc.additional_file || []);
this.response.body.reference = this.pdoc.reference;
this.response.body.additional_file = d.includes('additional_file') ? sortFiles(this.pdoc.additional_file || []) : [];
if (pjax) {
const { testdata, additional_file } = this.response.body;
const owner = await user.getById(domainId, this.pdoc.owner);
const args = {
testdata, additional_file, pdoc: this.pdoc, owner_udoc: owner, sidebar, can_edit: true,
};
const tasks = [];
if (d.includes('testdata')) tasks.push(this.renderHTML('partials/problem_files.html', { ...args, filetype: 'testdata' }));
if (d.includes('additional_file')) tasks.push(this.renderHTML('partials/problem_files.html', { ...args, filetype: 'additional_file' }));
if (!sidebar) tasks.push(this.renderHTML('partials/problem-sidebar-information.html', args));
this.response.body = {
fragments: (await Promise.all(tasks)).map((i) => ({ html: i })),
};
this.response.template = '';
} else this.response.template = 'problem_files.html';
this.response.pjax = d.map((i) => ['partials/problem_files.html', { filetype: i, sidebar, can_edit: true }]);
if (!sidebar) this.response.pjax.push(['partials/problem-sidebar-information.html', {}]);
this.response.template = 'problem_files.html';
Comment thread
undefined-moe marked this conversation as resolved.
}

async post() {
Expand Down
1 change: 1 addition & 0 deletions packages/hydrooj/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ export interface Tdoc extends Document {
_code?: string;
assign?: string[];
files?: FileInfo[];
privateFiles?: FileInfo[];
allowViewCode?: boolean;
allowPrint?: boolean;

Expand Down
7 changes: 4 additions & 3 deletions packages/ui-default/components/upload.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Classes } from '@blueprintjs/core';
import $ from 'jquery';
import React from 'react';
import { Dialog } from 'vj/components/dialog/index';
import Notification from 'vj/components/notification';
import {
Expand Down Expand Up @@ -82,9 +83,9 @@ export default async function uploadFiles(endpoint = '', files: File[] | FileLis
Notification.success(i18n('File uploaded successfully.'));
if (options.pjax) {
let params = '';
if (options.type) params += `? d = ${options.type} `;
if (options.sidebar) params += `${params ? '&' : '?'} sidebar = true`;
await pjax.request({ push: false, url: `${endpoint}${params || ''} ` });
if (options.type) params += `?d=${options.type}`;
if (options.sidebar) params += `${params ? '&' : '?'}sidebar=true`;
await pjax.request({ push: false, url: `${endpoint}${params || ''}` });
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
} catch (e) {
console.error(e);
Expand Down
1 change: 0 additions & 1 deletion packages/ui-default/locales/ko.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,6 @@ name: 이름
Name: 이름
Network error: Network error
Never expire: 영원히 만료되지 않음
New dataset: 새 데이터셋
New Email: 새 이메일
New file: 새 파일
New Password: 새 비밀번호
Expand Down
Loading
Loading