diff --git a/framework/framework/server.ts b/framework/framework/server.ts index c24ccfc76..00d83957f 100644 --- a/framework/framework/server.ts +++ b/framework/framework/server.ts @@ -23,7 +23,7 @@ import serializer from './serializer'; export { WebSocket, WebSocketServer } from 'ws'; -const kHandler = Symbol.for('hydro.handler'); +export const kHandler = Symbol.for('hydro.handler'); // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent export function encodeRFC5987ValueChars(str: string) { @@ -111,7 +111,7 @@ export interface UserModel { export interface HandlerCommon { } export class HandlerCommon { - static [kHandler] = true; + static [kHandler]: string | boolean = true; session: Record; args: Record; request: HydroRequest; @@ -313,7 +313,7 @@ function executeMiddlewareStack(context: any, middlewares: { name: string, func: return dispatch(0); } -interface WebServiceConfig { +export interface WebServiceConfig { keys: string[]; proxy: boolean; cors?: string; @@ -459,6 +459,7 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`); const h = new HandlerClass(ctx, this.ctx); ctx.handler = h; const method = ctx.method.toLowerCase(); + const name = (typeof HandlerClass[kHandler] === 'string' ? HandlerClass[kHandler] : HandlerClass.name).replace(/Handler$/, ''); try { const operation = (method === 'post' && ctx.request.body?.operation) ? `_${ctx.request.body.operation}`.replace(/_([a-z])/gm, (s) => s[1].toUpperCase()) @@ -479,7 +480,6 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`); throw new MethodNotAllowedError(method); } - const name = HandlerClass.name.replace(/Handler$/, ''); const steps = [ 'log/__init', 'init', 'handler/init', `handler/before-prepare/${name}#${method}`, `handler/before-prepare/${name}`, 'handler/before-prepare', @@ -516,7 +516,7 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`); } } catch (e) { try { - await this.ctx.serial(`handler/error/${HandlerClass.name.replace(/Handler$/, '')}`, h, e); + await this.ctx.serial(`handler/error/${name}`, h, e); await this.ctx.serial('handler/error', h, e); await h.onerror(e); } catch (err) { @@ -596,7 +596,7 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`); private register(type: 'route' | 'conn', routeName: string, path: string, HandlerClass: any, ...permPrivChecker) { if (!HandlerClass?.[kHandler] || !isClass(HandlerClass)) throw new Error('Invalid registration.'); - const name = HandlerClass.name; + const name = typeof HandlerClass[kHandler] === 'string' ? HandlerClass[kHandler] : HandlerClass.name; if (this.registrationCount[name] && this.registry[name] !== HandlerClass) { logger.warn('Route with name %s already exists.', name); } @@ -697,7 +697,7 @@ declare module 'cordis' { } } -export async function apply(ctx: Context, config) { +export async function apply(ctx: Context, config: WebServiceConfig) { ctx.provide('server', undefined, true); ctx.server = new WebService(ctx, config); } diff --git a/packages/fps-importer/index.ts b/packages/fps-importer/index.ts index ec5f65732..4a2386600 100644 --- a/packages/fps-importer/index.ts +++ b/packages/fps-importer/index.ts @@ -19,7 +19,7 @@ class FpsProblemImportHandler extends Handler { if (!result?.fps) throw new BadRequestError('Selected file is not a valid FPS problemset.'); for (const p of result.fps.item) { const markdown = [p.description?.[0], p.input?.[0], p.output?.[0], p.hint?.[0]].some((i) => i?.includes('[md]')); - const content = buildContent({ + let content = buildContent({ description: p.description?.[0], input: p.input?.[0], output: p.output?.[0], @@ -62,6 +62,14 @@ class FpsProblemImportHandler extends Handler { addTestdata(p.test_input[2 * i + 1], `${i + 1}`, 'out'); } } + if (p.img?.length) { + for (const img of p.img) { + const filename = String.random(8) + img.src[0].split('/').pop().split('.').pop(); + tasks.push(ProblemModel.addAdditionalFile(domainId, pid, filename, Buffer.from(img.base64[0], 'base64'))); + content = content.replace(img.src[0], `file://${filename}`); + } + await ProblemModel.edit(domainId, pid, { content }); + } await Promise.all(tasks); if (p.solution) { let s = ''; diff --git a/packages/hydrojudge/src/judge/default.ts b/packages/hydrojudge/src/judge/default.ts index 8d9b1ac75..6451b7566 100644 --- a/packages/hydrojudge/src/judge/default.ts +++ b/packages/hydrojudge/src/judge/default.ts @@ -44,7 +44,12 @@ function judgeCase(c: NormalizedCase) { user_stderr: fileIds.stderr ? { fileId: fileIds.stderr } : { content: '' }, score: c.score, detail, - env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() }, + env: { + ...ctx.env, + HYDRO_TESTCASE: c.id.toString(), + HYDRO_TIME_USAGE: time.toString(), + HYDRO_MEMORY_USAGE: Math.floor(memory / 1024).toString(), + }, })); } } else if (status === STATUS.STATUS_RUNTIME_ERROR && code && detail) { diff --git a/packages/hydrooj/src/handler/home.ts b/packages/hydrooj/src/handler/home.ts index d2eefa4e1..0a45bfdad 100644 --- a/packages/hydrooj/src/handler/home.ts +++ b/packages/hydrooj/src/handler/home.ts @@ -121,14 +121,12 @@ export class HomeHandler extends Handler { if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM)) return [[], {}]; const psdocs = await ProblemModel.getMultiStatus(domainId, { uid: this.user._id, star: true }) .sort('_id', 1).limit(limit).toArray(); - const psdict = {}; - for (const psdoc of psdocs) psdict[psdoc.docId] = psdoc; const pdict = await ProblemModel.getList( domainId, psdocs.map((pdoc) => pdoc.docId), this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || this.user._id, false, ); const pdocs = Object.keys(pdict).filter((i) => +i).map((i) => pdict[i]); - return [pdocs, psdict]; + return [pdocs]; } async getRecentProblems(domainId: string, limit = 10) { diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index 9d2194ec6..15170380c 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -186,18 +186,6 @@ export class ProblemMainHandler extends Handler { } } - @param('pid', Types.UnsignedInt) - async postStar(domainId: string, pid: number) { - await problem.setStar(domainId, pid, this.user._id, true); - this.back({ star: true }); - } - - @param('pid', Types.UnsignedInt) - async postUnstar(domainId: string, pid: number) { - await problem.setStar(domainId, pid, this.user._id, false); - this.back({ star: false }); - } - @param('pids', Types.NumericArray) @param('target', Types.String) async postCopy(domainId: string, pids: number[], target: string) { @@ -438,6 +426,12 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler { await problem.del(this.pdoc.domainId, this.pdoc.docId); this.response.redirect = this.url('problem_main'); } + + @param('star', Types.Boolean) + async postStar(domainId: string, star: boolean) { + await problem.setStar(domainId, this.pdoc.docId, this.user._id, star); + this.back({ star }); + } } export class ProblemSubmitHandler extends ProblemDetailHandler { diff --git a/packages/hydrooj/src/lib/i18n.ts b/packages/hydrooj/src/lib/i18n.ts index ed1da6fd9..fa1a7959c 100644 --- a/packages/hydrooj/src/lib/i18n.ts +++ b/packages/hydrooj/src/lib/i18n.ts @@ -7,6 +7,11 @@ declare module '../context' { i18n: I18nService; } } +declare module '../service/bus' { + interface EventMap { + 'app/i18n/update': (lang: string) => void; + } +} declare global { interface String { translate: (...languages: string[]) => string; @@ -22,6 +27,7 @@ class I18nService extends Service { this.ctx.effect(() => { translations[lang] ||= []; translations[lang].unshift(content); + this.ctx.emit('app/i18n/update', lang); return () => { translations[lang] = translations[lang].filter((i) => i !== content); }; diff --git a/packages/hydrooj/src/service/db.ts b/packages/hydrooj/src/service/db.ts index 6dee3f3fd..7693aa0ff 100644 --- a/packages/hydrooj/src/service/db.ts +++ b/packages/hydrooj/src/service/db.ts @@ -124,12 +124,12 @@ class MongoService { return [pageDocs, numPages, count]; } - async ranked>(cursor: FindCursor, equ: (a: T, b: T) => boolean): Promise<[number, T][]> { + async ranked>(cursor: T[] | FindCursor, equ: (a: T, b: T) => boolean): Promise<[number, T][]> { let last = null; let r = 0; let count = 0; const results = []; - const docs = await cursor.toArray(); + const docs = cursor instanceof Array ? cursor : await cursor.toArray(); for (const doc of docs) { if ((doc as any).unrank) { results.push([0, doc]); diff --git a/packages/onlyoffice/frontend/office.page.ts b/packages/onlyoffice/frontend/office.page.ts index d1fb097c4..8b9e63ee6 100644 --- a/packages/onlyoffice/frontend/office.page.ts +++ b/packages/onlyoffice/frontend/office.page.ts @@ -1,19 +1,23 @@ /* eslint-disable github/array-foreach */ import { - $, addPage, AutoloadPage, request, + $, addPage, AutoloadPage, i18n, Notification, request, } from '@hydrooj/ui-default'; - /* global DocsAPI */ let loaded = false; -async function load() { - if (loaded) return Promise.resolve(); +function load(): Promise { + if (loaded) return Promise.resolve(true); + if (!UiContext.onlyofficeApi) { + console.error('Onlyoffice api not set'); + Notification.error(i18n('onlyoffice.not_configured')); + return Promise.resolve(false); + } return new Promise((resolve, reject) => { const scriptElement = document.createElement('script'); scriptElement.src = UiContext.onlyofficeApi; scriptElement.async = true; document.head.appendChild(scriptElement); - scriptElement.onload = resolve; + scriptElement.onload = () => resolve(true); scriptElement.onerror = reject; loaded = true; }); @@ -40,9 +44,16 @@ const getEles = (types: string[]) => types.flatMap((type) => $(`div[data-${type} addPage(new AutoloadPage('onlyoffice', async () => { const all = getEles(['doc', 'docx', 'cell', 'xls', 'xlsx', 'slide', 'ppt', 'pptx', 'pdf']); - if (all.length) await load(); - getEles(['doc', 'docx']).forEach(loader('word')); - getEles(['cell', 'xls', 'xlsx']).forEach(loader('cell')); - getEles(['slide', 'ppt', 'pptx']).forEach(loader('slide')); - getEles(['pdf']).forEach(loader('pdf')); + if (!all.length) return; + try { + const result = await load(); + if (!result) return; + getEles(['doc', 'docx']).forEach(loader('word')); + getEles(['cell', 'xls', 'xlsx']).forEach(loader('cell')); + getEles(['slide', 'ppt', 'pptx']).forEach(loader('slide')); + getEles(['pdf']).forEach(loader('pdf')); + } catch (e) { + console.error(`Failed to initialize onlyoffice: ${e.message}`); + Notification.error(i18n('onlyoffice.inialize_fail', e.message)); + } })); diff --git a/packages/onlyoffice/index.ts b/packages/onlyoffice/index.ts index a5b6963ae..46b614154 100644 --- a/packages/onlyoffice/index.ts +++ b/packages/onlyoffice/index.ts @@ -84,6 +84,18 @@ export function apply(ctx: Context) { enumerable: false, }); }); + + ctx.inject(['i18n'], (c) => { + c.i18n.load('en', { + 'onlyoffice.not_configured': 'Onlyoffice API not configured.', + 'onlyoffice.initialize_fail': 'Failed to initialize onlyoffice: {0}', + }); + c.i18n.load('zh', { + 'onlyoffice.not_configured': 'Onlyoffice 未配置。自行安装 onlyoffice 软件或是使用外部提供的 API。', + 'onlyoffice.initialize_fail': '初始化 onlyoffice 失败: {0}', + }); + }); + ctx.Route('onlyoffice-jwt', '/onlyoffice-jwt', OnlyofficeJWTHandler); if (SystemModel.get('onlyoffice.pdf')) { ctx.provideModule('richmedia', 'pdf', { diff --git a/packages/ui-default/backendlib/builder.ts b/packages/ui-default/backendlib/builder.ts index f79137b0d..23fe6b739 100644 --- a/packages/ui-default/backendlib/builder.ts +++ b/packages/ui-default/backendlib/builder.ts @@ -145,5 +145,6 @@ export async function apply(ctx: Context) { ctx.on('system/setting', () => triggerHotUpdate()); ctx.on('app/watch/change', triggerHotUpdate); ctx.on('app/watch/unlink', triggerHotUpdate); + ctx.on('app/i18n/update', debouncedBuildUI); debouncedBuildUI(); } diff --git a/packages/ui-default/components/star/star.page.ts b/packages/ui-default/components/star/star.page.ts index e8f534bb7..e823ca0bf 100644 --- a/packages/ui-default/components/star/star.page.ts +++ b/packages/ui-default/components/star/star.page.ts @@ -1,6 +1,6 @@ import $ from 'jquery'; import { AutoloadPage } from 'vj/misc/Page'; -import { request } from 'vj/utils'; +import { request, tpl } from 'vj/utils'; function setStarButtonState($starButton: JQuery, star: boolean) { if (star) { @@ -11,6 +11,29 @@ function setStarButtonState($starButton: JQuery, star: boolean) { } const starPage = new AutoloadPage('starPage', () => { + function render(container) { + const eles = $(container).find('[data-star]'); + for (const el of eles) { + if ($(el).find('.star-form')) continue; + const pid = $(el).closest('[data-pid]').data('pid'); + const star = $(el).data('star') === 'true' ? 1 : 0; + if (!pid) continue; + $(tpl`
+ + + +
`); + } + } + + render(document); + $(document).on('vjContentNew', (ev) => { + render(ev.target); + }); + $(document).on('click', '.star', (ev) => { const $button = $(ev.currentTarget); const currentState = $button.hasClass('activated'); diff --git a/packages/ui-default/pages/problem_sidebar.page.ts b/packages/ui-default/pages/problem_sidebar.page.ts index c564b78cc..0857057bd 100644 --- a/packages/ui-default/pages/problem_sidebar.page.ts +++ b/packages/ui-default/pages/problem_sidebar.page.ts @@ -7,7 +7,8 @@ import { i18n, request, tpl } from 'vj/utils'; const page = new NamedPage([ 'problem_create', 'problem_edit', 'problem_solution', 'problem_submit', - 'problem_files', 'problem_detail', 'discussion_node', 'discussion_detail', + 'problem_config', 'problem_statistics', 'problem_files', 'problem_detail', + 'discussion_node', 'discussion_detail', ], () => { $(document).on('click', '[name="problem-sidebar__show-category"]', (ev) => { $(ev.currentTarget).hide(); diff --git a/packages/ui-default/templates/contest_manage.html b/packages/ui-default/templates/contest_manage.html index 829a7bad9..a7af2a9a9 100644 --- a/packages/ui-default/templates/contest_manage.html +++ b/packages/ui-default/templates/contest_manage.html @@ -2,6 +2,7 @@ {% import "components/contest.html" as contest with context %} {% import "components/problem.html" as problem with context %} {% block content %} +{{ set(UiContext, 'tdoc', tdoc) }}
diff --git a/packages/ui-default/templates/partials/homepage/starred_problems.html b/packages/ui-default/templates/partials/homepage/starred_problems.html index 9636c0a50..831456554 100644 --- a/packages/ui-default/templates/partials/homepage/starred_problems.html +++ b/packages/ui-default/templates/partials/homepage/starred_problems.html @@ -1,5 +1,4 @@ {% set pdocs = payload[0] %} -{% set psdict = payload[1] %} {% if pdocs.length %}
@@ -7,15 +6,9 @@
{%- for pdoc in pdocs -%} -
- - - -
- {{ problem.render_problem_title(pdoc, show_tags=false, show_invisible_flag=false) }}
+

+ {{ problem.render_problem_title(pdoc, show_tags=false, show_invisible_flag=false) }} +

{%- endfor -%}
diff --git a/packages/ui-default/templates/partials/problem_list.html b/packages/ui-default/templates/partials/problem_list.html index 254889fdf..0e9f8a017 100644 --- a/packages/ui-default/templates/partials/problem_list.html +++ b/packages/ui-default/templates/partials/problem_list.html @@ -54,17 +54,7 @@ {% endif %} {% endif %} - - {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} -
- - - -
- {% endif %} + {{ problem.render_problem_title(pdoc) }} {{ pdoc.nAccept }} / {{ pdoc.nSubmit }} diff --git a/packages/ui-default/templates/partials/training_detail.html b/packages/ui-default/templates/partials/training_detail.html index a0e9c2797..7c954caa1 100644 --- a/packages/ui-default/templates/partials/training_detail.html +++ b/packages/ui-default/templates/partials/training_detail.html @@ -103,7 +103,7 @@ {% set pdoc=pdict[pid] %} {% set psdoc=psdict[pid] %} {% set self_psdoc=selfPsdict[pid] %} - + {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} {% if psdoc['rid'] %} {{ record.render_status_td(psdoc, rid_key='rid', short=should_compare) }} @@ -118,17 +118,7 @@ {% endif %} {% endif %} {% endif %} - - {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} -
- - - -
- {% endif %} + {{ problem.render_problem_title( pdoc, invalid=not tsdoc['enroll'] or nsdict[node['_id']]['isInvalid'], diff --git a/packages/ui-default/templates/problem_detail.html b/packages/ui-default/templates/problem_detail.html index fb107a43f..7335a5a47 100644 --- a/packages/ui-default/templates/problem_detail.html +++ b/packages/ui-default/templates/problem_detail.html @@ -36,9 +36,9 @@ {% endif %} {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) and not tdoc %} -
- - + + +