diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 80add5801..b8444caae 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "2.23.31", + "version": "2.24.0", "bin": "bin/hydrooj.js", "main": "dist/loader.js", "typings": "dist/loader.d.ts", diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index d5266c9f7..99da25b13 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -136,9 +136,9 @@ class ContestBoardcastHandler extends Handler { class ContestScoreboardHandler extends Handler { @param('tid', Types.ObjectID) - @param('page', Types.PositiveInt) + @param('page', Types.PositiveInt, true) async get(domainId: string, tid: ObjectID, page = 1) { - const [tdoc, rows, udict] = await contest.getScoreboard(domainId, tid, false, page); + const [tdoc, rows, udict, , nPages] = await contest.getScoreboard.call(this, domainId, tid, false, page); const path = [ ['Hydro', 'homepage'], ['contest_main', 'contest_main'], @@ -147,7 +147,7 @@ class ContestScoreboardHandler extends Handler { ]; this.response.template = 'contest_scoreboard.html'; this.response.body = { - tdoc, rows, path, udict, + tdoc, rows, path, udict, nPages, page, }; } } @@ -156,11 +156,12 @@ class ContestScoreboardDownloadHandler extends Handler { @param('tid', Types.ObjectID) @param('ext', Types.Range(['csv', 'html'])) async get(domainId: string, tid: ObjectID, ext: string) { + await this.limitRate('scoreboard_download', 120, 3); const getContent = { csv: async (rows) => `\uFEFF${rows.map((c) => (c.map((i) => i.value).join(','))).join('\n')}`, html: (rows) => this.renderHTML('contest_scoreboard_download_html.html', { rows }), }; - const [tdoc, rows] = await contest.getScoreboard(domainId, tid, true, 0); + const [tdoc, rows] = await contest.getScoreboard.call(this, domainId, tid, true, 0); this.binary(await getContent[ext](rows), `${tdoc.title}.${ext}`); } } diff --git a/packages/hydrooj/src/handler/homework.ts b/packages/hydrooj/src/handler/homework.ts index 85c3755df..bd9870d00 100644 --- a/packages/hydrooj/src/handler/homework.ts +++ b/packages/hydrooj/src/handler/homework.ts @@ -356,8 +356,9 @@ class HomeworkScoreboardHandler extends Handler { @param('tid', Types.ObjectID) @param('page', Types.PositiveInt, true) async get(domainId: string, tid: ObjectID, page = 1) { - const [tdoc, rows, udict] = await contest.getScoreboard( - domainId, tid, false, page, document.TYPE_HOMEWORK, + const [tdoc, rows, udict, , nPages] = await contest.getScoreboard.call( + this, domainId, tid, false, + page, document.TYPE_HOMEWORK, ); const path = [ ['Hydro', 'homepage'], @@ -367,7 +368,7 @@ class HomeworkScoreboardHandler extends Handler { ]; this.response.template = 'contest_scoreboard.html'; this.response.body = { - tdoc, rows, path, udict, page_name: 'homework_scoreboard', + tdoc, rows, path, udict, page, nPages, page_name: 'homework_scoreboard', }; } } @@ -376,12 +377,13 @@ class HomeworkScoreboardDownloadHandler extends Handler { @param('tid', Types.ObjectID) @param('ext', Types.Name) async get(domainId: string, tid: ObjectID, ext: string) { + await this.limitRate('scoreboard_download', 120, 3); const getContent = { csv: (rows) => `\uFEFF${rows.map((c) => (c.map((i) => i.value).join(','))).join('\n')}`, html: (rows) => this.renderHTML('contest_scoreboard_download_html.html', { rows }), }; if (!getContent[ext]) throw new ValidationError('ext'); - const [tdoc, rows] = await contest.getScoreboard(domainId, tid, true, 0, document.TYPE_HOMEWORK); + const [tdoc, rows] = await contest.getScoreboard.call(this, domainId, tid, true, 0, document.TYPE_HOMEWORK); this.binary(await getContent[ext](rows), `${tdoc.title}.${ext}`); } } diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 7cbeb6517..e49a7f97e 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -409,7 +409,7 @@ export interface ContestRule { scoreboard: ( isExport: boolean, _: (s: string) => string, tdoc: Tdoc<30 | 60>, pdict: Pdict, cursor: Cursor, page: number, - ) => Promise<[board: ScoreboardRow[], udict: Udict]>; + ) => Promise<[board: ScoreboardRow[], udict: Udict, nPages: number]>; ranked: (tdoc: Tdoc<30 | 60>, cursor: Cursor) => Promise; } diff --git a/packages/hydrooj/src/lib/rank.ts b/packages/hydrooj/src/lib/rank.ts index c5f71cceb..60e8089c1 100644 --- a/packages/hydrooj/src/lib/rank.ts +++ b/packages/hydrooj/src/lib/rank.ts @@ -3,24 +3,25 @@ import paginate from './paginate'; type G = Promise<[[number, T], () => G] | []>; interface ranked { - (cursor: Cursor, equ: (a: T, b: T) => boolean, page: number): Promise<[number, T][]>; + (cursor: Cursor, equ: (a: T, b: T) => boolean, page: number): Promise<[[number, T][], number]>; iterate(cursor: Cursor, equ: (a: T, b: T) => boolean): G; - all(cursor: Cursor, equ: (a: T, b: T) => boolean): Promise<[number, T][]>; + all(cursor: Cursor, equ: (a: T, b: T) => boolean): Promise<[[number, T][], 1]>; } const ranked: ranked = async function rankedPage(cursor, equ = (a, b) => a === b, page) { + if (!page) return await ranked.all(cursor, equ); let last = null; let r = (page - 1) * 100; let count = r; const results = []; - const [docs] = await paginate(cursor, page, 100); + const [docs, nPages] = await paginate(cursor, page, 100); for (const doc of docs) { count++; - if (count === 1 || !equ(last, doc)) r = count; + if (!last || !equ(last, doc)) r = count; last = doc; results.push([r, doc]); } - return results; + return [results, nPages]; }; ranked.iterate = async function rankedIterate(cursor: Cursor, equ = (a, b) => a === b) { let last = null; @@ -30,7 +31,7 @@ ranked.iterate = async function rankedIterate(cursor: Cursor, equ = (a, b) if (!await cursor.hasNext()) return []; const doc = await cursor.next(); count++; - if (count === 1 || !equ(last, doc)) r = count; + if (!last || !equ(last, doc)) r = count; last = doc; return [[r, doc], getNext]; } @@ -44,11 +45,11 @@ ranked.all = async function rankedAll(cursor, equ = (a, b) => a === b) { const docs = await cursor.toArray(); for (const doc of docs) { count++; - if (count === 1 || !equ(last, doc)) r = count; + if (!last || !equ(last, doc)) r = count; last = doc; results.push([r, doc]); } - return results; + return [results, 1]; }; global.Hydro.lib.rank = ranked; diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index 54fb7df22..440f0ad0f 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -53,7 +53,7 @@ const acm: ContestRule = { return { accept, time, detail }; }, async scoreboard(isExport, _, tdoc, pdict, cursor, page) { - const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score, page); + const [rankedTsdocs, nPages] = await ranked(cursor, (a, b) => a.score === b.score, page); const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid); const udict = await user.getList(tdoc.domainId, uids); const columns: ScoreboardRow = [ @@ -139,7 +139,7 @@ const acm: ContestRule = { row.raw = tsdoc; rows.push(row); } - return [rows, udict]; + return [rows, udict, nPages]; }, async ranked(tdoc, cursor) { return await ranked.all(cursor, (a, b) => a.score === b.score); @@ -162,7 +162,7 @@ const oi: ContestRule = { showScoreboard: (tdoc, now) => now > tdoc.endAt, showRecord: (tdoc, now) => now > tdoc.endAt, async scoreboard(isExport, _, tdoc, pdict, cursor, page) { - const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score, page); + const [rankedTsdocs, nPages] = await ranked(cursor, (a, b) => a.score === b.score, page); const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid); const udict = await user.getList(tdoc.domainId, uids); const columns: ScoreboardNode[] = [ @@ -205,7 +205,7 @@ const oi: ContestRule = { } rows.push(row); } - return [rows, udict]; + return [rows, udict, nPages]; }, async ranked(tdoc, cursor) { return await ranked.all(cursor, (a, b) => a.score === b.score); @@ -268,7 +268,7 @@ const homework: ContestRule = { showScoreboard: () => true, showRecord: () => true, async scoreboard(isExport, _, tdoc, pdict, cursor, page) { - const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score, page); + const [rankedTsdocs, nPages] = await ranked(cursor, (a, b) => a.score === b.score, page); const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid); const udict = await user.getList.call(this, tdoc.domainId, uids); const columns: ScoreboardNode[] = [ @@ -358,7 +358,7 @@ const homework: ContestRule = { } rows.push(row); } - return [rows, udict]; + return [rows, udict, nPages]; }, async ranked(tdoc, cursor) { return await ranked.all(cursor, (a, b) => a.score === b.score); @@ -564,17 +564,17 @@ export function canShowScoreboard(tdoc: Tdoc<30 | 60>, allowPermOverride = true) export async function getScoreboard( domainId: string, tid: ObjectID, isExport = false, page: number, docType: 30 | 60 = document.TYPE_CONTEST, -): Promise<[Tdoc<30 | 60>, ScoreboardRow[], Udict, Pdict]> { +): Promise<[Tdoc<30 | 60>, ScoreboardRow[], Udict, Pdict, number]> { const tdoc = await get(domainId, tid, docType); - if (!this.canShowScoreboard(tdoc)) throw new ContestScoreboardHiddenError(tid); - if (!this.canShowRecord(tdoc)) throw new ContestScoreboardHiddenError(tid); + if (!canShowScoreboard.call(this, tdoc)) throw new ContestScoreboardHiddenError(tid); + if (!canShowRecord.call(this, tdoc)) throw new ContestScoreboardHiddenError(tid); const tsdocs = await getMultiStatus(domainId, { docId: tid }, docType).sort(RULES[tdoc.rule].statusSort); const pdict = await problem.getList(domainId, tdoc.pids, true); - const [rows, udict] = await RULES[tdoc.rule].scoreboard( + const [rows, udict, nPages] = await RULES[tdoc.rule].scoreboard( isExport, this.translate.bind(this), tdoc, pdict, tsdocs, page, ); - return [tdoc, rows, udict, pdict]; + return [tdoc, rows, udict, pdict, nPages]; } export async function verifyProblems(domainId: string, pids: Array) { diff --git a/packages/hydrooj/src/model/user.ts b/packages/hydrooj/src/model/user.ts index b1bb798bb..ec6b9bf27 100644 --- a/packages/hydrooj/src/model/user.ts +++ b/packages/hydrooj/src/model/user.ts @@ -17,7 +17,7 @@ import * as bus from '../service/bus'; const coll: Collection = db.collection('user'); const logger = new Logger('model/user'); -const cache = new LRU(500); +const cache = new LRU({ max: 500, maxAge: 300 * 1000 }); function deleteUserCache(udoc: User | Udoc) { if (udoc) { diff --git a/packages/hydrooj/src/service/server.ts b/packages/hydrooj/src/service/server.ts index ba7ee8983..20776907d 100644 --- a/packages/hydrooj/src/service/server.ts +++ b/packages/hydrooj/src/service/server.ts @@ -468,7 +468,7 @@ export class Handler extends HandlerCommon { this.response.body = data; this.response.template = null; this.response.type = 'application/octet-stream'; - if (name) this.response.disposition = `attachment; filename="${name}"`; + if (name) this.response.disposition = `attachment; filename="${encodeURIComponent(name)}"`; } async getSession() { @@ -567,7 +567,7 @@ export class Handler extends HandlerCommon { } async putResponse() { - if (this.response.disposition) this.ctx.set('Content-Disposition', encodeURIComponent(this.response.disposition)); + if (this.response.disposition) this.ctx.set('Content-Disposition', this.response.disposition); if (!this.response.body) return; if (this.response.redirect && !this.request.json) { this.ctx.response.type = 'application/octet-stream'; diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 876a4f9b7..587f3a866 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.6.22", + "version": "4.6.23", "author": "undefined ", "license": "AGPL-3.0", "main": "hydro.js", diff --git a/packages/ui-default/templates/contest_detail.html b/packages/ui-default/templates/contest_detail.html index 6b40b5349..aa28d4f3b 100644 --- a/packages/ui-default/templates/contest_detail.html +++ b/packages/ui-default/templates/contest_detail.html @@ -45,7 +45,7 @@ {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} {% if psdict[pid] and psdict[pid].rid %} {% set rdoc = rdict[psdict[pid].rid] %} - {% if handler.canShowRecord(tdoc) %} + {% if model.contest.canShowRecord.call(handler, tdoc) %} {{ record.render_status_td(rdoc) }} {% else %} {{ _('Submitted') }} diff --git a/packages/ui-default/templates/contest_scoreboard.html b/packages/ui-default/templates/contest_scoreboard.html index 1e3890cef..fbec2a835 100644 --- a/packages/ui-default/templates/contest_scoreboard.html +++ b/packages/ui-default/templates/contest_scoreboard.html @@ -1,3 +1,4 @@ +{% import "components/paginator.html" as paginator with context %} {% extends "layout/basic.html" %} {% set type = 'homework' if tdoc.docType == 60 else 'contest' %} {% block content %} @@ -32,7 +33,7 @@ - {%- for row in rows -%}{% if loop.index0>0 %} + {%- for row in rows -%}{% if loop.index0 > 0 %} {%- for column in row -%} @@ -49,6 +50,7 @@ {% endif %}{%- endfor -%} + {{ paginator.render(page, nPages) }} diff --git a/packages/ui-default/templates/homework_detail.html b/packages/ui-default/templates/homework_detail.html index 8021b75d1..34556931e 100644 --- a/packages/ui-default/templates/homework_detail.html +++ b/packages/ui-default/templates/homework_detail.html @@ -44,7 +44,7 @@ {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} {% if psdict[pid] and psdict[pid]['rid'] %} {% set rdoc = rdict[psdict[pid]['rid']] %} - {% if handler.canShowRecord(tdoc) %} + {% if model.contest.canShowRecord.call(handler, tdoc) %} {{ record.render_status_td(rdoc) }} {% else %} {{ _('Submitted') }} diff --git a/packages/ui-default/templates/partials/contest_sidebar.html b/packages/ui-default/templates/partials/contest_sidebar.html index 0f9d1dbc1..df9a96580 100644 --- a/packages/ui-default/templates/partials/contest_sidebar.html +++ b/packages/ui-default/templates/partials/contest_sidebar.html @@ -43,11 +43,11 @@ {% endif %} {% endif %} - {% if handler.canShowScoreboard(tdoc, False) %} + {% if model.contest.canShowScoreboard.call(handler, tdoc, False) %} - {% elif handler.canViewHiddenScoreboard(tdoc) %} + {% elif model.contest.canViewHiddenScoreboard.call(handler, tdoc) %} diff --git a/packages/ui-default/templates/partials/homework_sidebar.html b/packages/ui-default/templates/partials/homework_sidebar.html index 7e4bb4b48..029c3386e 100644 --- a/packages/ui-default/templates/partials/homework_sidebar.html +++ b/packages/ui-default/templates/partials/homework_sidebar.html @@ -43,11 +43,11 @@ {% endif %} {% endif %} - {% if handler.canShowScoreboard(tdoc, false) %} + {% if model.contest.canShowScoreboard.call(handler, tdoc, false) %} - {% elif handler.canViewHiddenScoreboard(tdoc) %} + {% elif model.contest.canViewHiddenScoreboard(handler, tdoc) %}