diff --git a/package.json b/package.json index c2b4972ba..734833439 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@types/autocannon": "^7.6.1", "@types/cross-spawn": "^6.0.2", "@types/mocha": "^9.1.1", - "@types/node": "^18.0.0", + "@types/node": "^18.0.3", "@types/semver": "^7.3.10", "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^5.29.0", @@ -57,11 +57,11 @@ "latest-version": "6.0.0", "mocha": "^10.0.0", "mongodb": "^3.7.3", - "mongodb-memory-server": "^8.7.1", + "mongodb-memory-server": "^8.7.2", "ora": "5.4.1", "semver": "^7.3.7", - "supertest": "^6.2.3", - "typedoc": "^0.22.18", + "supertest": "^6.2.4", + "typedoc": "^0.23.7", "typescript": "4.6.4" }, "resolutions": { diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 59c3b1273..2f4272825 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "3.14.10", + "version": "3.14.11", "bin": "bin/hydrooj.js", "main": "src/loader", "module": "src/loader", @@ -12,7 +12,7 @@ }, "preferUnplugged": true, "dependencies": { - "@graphql-tools/schema": "^8.4.0", + "@graphql-tools/schema": "^8.5.0", "@hydrooj/utils": "workspace:*", "adm-zip": "0.5.5", "ajv": "^8.11.0", @@ -35,13 +35,13 @@ "koa-router": "^10.1.1", "koa-static-cache": "^5.1.4", "lodash": "^4.17.21", - "lru-cache": "7.9.0", + "lru-cache": "7.12.0", "mime-types": "^2.1.35", "minio": "7.0.25", "moment-timezone": "^0.5.34", "mongodb": "^3.7.3", "nanoid": "^3.3.4", - "nodemailer": "^6.7.5", + "nodemailer": "^6.7.7", "notp": "^2.0.3", "p-queue": "^7.2.0", "path-to-regexp": "^6.2.1", @@ -62,7 +62,7 @@ "@types/koa-router": "^7.4.4", "@types/koa-static-cache": "^5.1.1", "@types/lodash": "^4.14.182", - "@types/lru-cache": "^7.6.1", + "@types/lru-cache": "^7.10.10", "@types/mime-types": "^2.1.1", "@types/minio": "^7.0.13", "@types/mongodb": "^3.6.20", @@ -72,7 +72,7 @@ "@types/serialize-javascript": "^5.0.2", "@types/source-map-support": "^0.5.4", "@types/superagent": "^4.1.15", - "moment": "^2.29.3", + "moment": "^2.29.4", "pm2": "^5.2.0" }, "peerDependencies": { diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index d7be8d284..8e78ac35e 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -71,7 +71,7 @@ export class ContestListHandler extends Handler { async get(domainId: string, rule = '', page = 1) { const cursor = contest.getMulti(domainId, rule ? { rule } : { rule: { $ne: 'homework' } }); const qs = rule ? `rule=${rule}` : ''; - const [tdocs, tpcount] = await paginate(cursor, page, system.get('pagination.contest')); + const [tdocs, tpcount] = await paginate(cursor, page, system.get('pagination.contest')); const tids = []; for (const tdoc of tdocs) tids.push(tdoc.docId); const tsdict = await contest.getListStatus(domainId, this.user._id, tids); @@ -82,47 +82,58 @@ export class ContestListHandler extends Handler { } } -export class ContestDetailHandler extends Handler { - @param('tid', Types.ObjectID) - @param('page', Types.PositiveInt, true) - async prepare(domainId: string, tid: ObjectID) { - const tdoc = await contest.get(domainId, tid); - if (tdoc.assign?.length && !this.user.own(tdoc)) { - const groups = await user.listGroup(domainId, this.user._id); - if (!Set.intersection(tdoc.assign, groups.map((i) => i.name)).size) { - throw new ForbiddenError('You are not assigned.'); - } - } - } +export class ContestDetailBaseHandler extends Handler { + tdoc?: Tdoc<30>; + tsdoc?: any; - @param('tid', Types.ObjectID) - @param('page', Types.PositiveInt, true) - async get(domainId: string, tid: ObjectID, page = 1) { - const [tdoc, tsdoc] = await Promise.all([ + @param('tid', Types.ObjectID, true) + async __prepare(domainId: string, tid: ObjectID) { + if (!tid) return; // ProblemDetailHandler also extends from ContestDetailBaseHandler + [this.tdoc, this.tsdoc] = await Promise.all([ contest.get(domainId, tid), contest.getStatus(domainId, tid, this.user._id), ]); + if (this.tdoc.assign?.length && !this.user.own(this.tdoc)) { + const groups = await user.listGroup(domainId, this.user._id); + if (!Set.intersection(this.tdoc.assign, groups.map((i) => i.name)).size) { + throw new ForbiddenError('You are not assigned.'); + } + } + if (this.tdoc.duration && this.tsdoc?.attend) { + this.tsdoc.endAt = moment(this.tsdoc.startAt).add(this.tdoc.duration, 'hours').toDate(); + } + } +} + +export class ContestDetailHandler extends ContestDetailBaseHandler { + @param('tid', Types.ObjectID) + async prepare(domainId: string, tid: ObjectID) { + if (contest.RULES[this.tdoc.rule].hidden) throw new ContestNotFoundError(domainId, tid); + } + + @param('tid', Types.ObjectID) + async get(domainId: string, tid: ObjectID) { this.response.template = 'contest_detail.html'; - const udict = await user.getList(domainId, [tdoc.owner]); + const udict = await user.getList(domainId, [this.tdoc.owner]); this.response.body = { - tdoc, tsdoc, udict, page, + tdoc: this.tdoc, tsdoc: this.tsdoc, udict, }; - if (contest.isNotStarted(tdoc)) return; + if (contest.isNotStarted(this.tdoc)) return; if (!this.request.json) { this.response.body.tdoc.content = this.response.body.tdoc.content - .replace(/\(file:\/\//g, `(./${tdoc.docId}/file/`) - .replace(/="file:\/\//g, `="./${tdoc.docId}/file/`); + .replace(/\(file:\/\//g, `(./${this.tdoc.docId}/file/`) + .replace(/="file:\/\//g, `="./${this.tdoc.docId}/file/`); } - const pdict = await problem.getList(domainId, tdoc.pids, true, undefined, undefined, problem.PROJECTION_CONTEST_LIST); + const pdict = await problem.getList(domainId, this.tdoc.pids, true, undefined, undefined, problem.PROJECTION_CONTEST_LIST); const psdict = {}; let rdict = {}; - if (tsdoc) { - if (tsdoc.attend && !tsdoc.startAt && contest.isOngoing(tdoc)) { + if (this.tsdoc) { + if (this.tsdoc.attend && !this.tsdoc.startAt && contest.isOngoing(this.tdoc)) { await contest.setStatus(domainId, tid, this.user._id, { startAt: new Date() }); - tsdoc.startAt = new Date(); + this.tsdoc.startAt = new Date(); } - for (const pdetail of tsdoc.journal || []) psdict[pdetail.pid] = pdetail; - if (contest.canShowSelfRecord.call(this, tdoc)) { + for (const pdetail of this.tsdoc.journal || []) psdict[pdetail.pid] = pdetail; + if (contest.canShowSelfRecord.call(this, this.tdoc)) { rdict = await record.getList(domainId, Object.values(psdict).map((i: any) => i.rid)); } else { for (const i in psdict) rdict[psdict[i].rid] = { _id: psdict[i].rid }; @@ -134,17 +145,15 @@ export class ContestDetailHandler extends Handler { @param('tid', Types.ObjectID) @param('code', Types.String, true) async postAttend(domainId: string, tid: ObjectID, code = '') { - const tdoc = await contest.get(domainId, tid); - if (contest.isDone(tdoc)) throw new ContestNotLiveError(tid); - if (tdoc._code && code !== tdoc._code) throw new InvalidTokenError(code); + if (contest.isDone(this.tdoc)) throw new ContestNotLiveError(tid); + if (this.tdoc._code && code !== this.tdoc._code) throw new InvalidTokenError(code); await contest.attend(domainId, tid, this.user._id); this.back(); } @param('tid', Types.ObjectID) async postDelete(domainId: string, tid: ObjectID) { - const tdoc = await contest.get(domainId, tid); - if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST); + if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST); await contest.del(domainId, tid); await record.updateMulti(domainId, { domainId, contest: tid }, undefined, undefined, { contest: '' }); await TaskModel.deleteMany({ @@ -154,26 +163,18 @@ export class ContestDetailHandler extends Handler { } } -export class ContestBroadcastHandler extends Handler { - @param('tid', Types.ObjectID) - async get(domainId: string, tid: ObjectID) { - const tdoc = await contest.get(domainId, tid); - if (!this.user.own(tdoc)) throw new PermissionError('Boardcast Message'); - const path = [ - ['Hydro', 'homepage'], - ['contest_main', 'contest_main'], - [tdoc.title, 'contest_detail', { tid }, true], - ['contest_broadcast'], - ]; +export class ContestBroadcastHandler extends ContestDetailBaseHandler { + async prepare() { + if (!this.user.own(this.tdoc)) throw new PermissionError('Boardcast Message'); + } + + async get() { this.response.template = 'contest_broadcast.html'; - this.response.body = path; } @param('tid', Types.ObjectID) @param('content', Types.Content) async post(domainId: string, tid: ObjectID, content: string) { - const tdoc = await contest.get(domainId, tid); - if (!this.user.own(tdoc)) throw new PermissionError('Broadcast Message'); const tsdocs = await contest.getMultiStatus(domainId, { docId: tid }).toArray(); const uids: number[] = Array.from(new Set(tsdocs.map((tsdoc) => tsdoc.uid))); await Promise.all( @@ -183,49 +184,44 @@ export class ContestBroadcastHandler extends Handler { } } -export class ContestScoreboardHandler extends Handler { +export class ContestScoreboardHandler extends ContestDetailBaseHandler { @param('tid', Types.ObjectID) @param('page', Types.PositiveInt, true) + @param('ext', Types.Range(['csv', 'html']), true) @param('ignoreLock', Types.Boolean, true) - async get(domainId: string, tid: ObjectID, page = 1, ignoreLock = false) { - const tdoc = await contest.get(domainId, tid); - if (ignoreLock && !this.user.own(tdoc)) { - this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); + async get(domainId: string, tid: ObjectID, page = 1, ext = '', ignoreLock = false) { + if (ignoreLock && !this.user.own(this.tdoc)) { + this.checkPerm(this.tdoc.rule === 'homework' + ? PERM.PERM_VIEW_HOMEWORK_HIDDEN_SCOREBOARD + : PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); } - const pdict = await problem.getList(domainId, tdoc.pids, true, undefined, false, [ + if (ext) { + await this.exportScoreboard(domainId, tid, ext, ignoreLock); + return; + } + const pdict = await problem.getList(domainId, this.tdoc.pids, true, undefined, false, [ // Problem statistics display is allowed as we can view submission info in scoreboard. ...problem.PROJECTION_CONTEST_LIST, 'nSubmit', 'nAccept', ]); const [, rows, udict, , nPages] = await contest.getScoreboard.call(this, domainId, tid, false, page, ignoreLock); - const path = [ - ['Hydro', 'homepage'], - ['contest_main', 'contest_main'], - [tdoc.title, 'contest_detail', { tid }, true], - ['contest_scoreboard', null], - ]; this.response.template = 'contest_scoreboard.html'; + // eslint-disable-next-line @typescript-eslint/naming-convention + const page_name = this.tdoc.rule === 'homework' + ? 'homework_scoreboard' + : 'contest_scoreboard'; this.response.body = { - tdoc, rows, path, udict, nPages, page, pdict, + tdoc: this.tdoc, rows, udict, nPages, page, pdict, page_name, }; } -} -export class ContestScoreboardDownloadHandler extends Handler { - @param('tid', Types.ObjectID) - @param('ext', Types.Range(['csv', 'html'])) - @param('ignoreLock', Types.Boolean, true) - async get(domainId: string, tid: ObjectID, ext: string, ignoreLock = false) { + async exportScoreboard(domainId: string, tid: ObjectID, ext: string, ignoreLock: boolean) { await this.limitRate('scoreboard_download', 120, 3); const getContent = { csv: async (rows) => `\uFEFF${rows.map((c) => (c.map((i) => i.value?.toString().replace(/\n/g, ' ')).join(','))).join('\n')}`, html: (rows, tdoc) => this.renderHTML('contest_scoreboard_download_html.html', { rows, tdoc }), }; - const tdoc = await contest.get(domainId, tid); - if (ignoreLock && !this.user.own(tdoc)) { - this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); - } const [, rows] = await contest.getScoreboard.call(this, domainId, tid, true, 0, ignoreLock); - this.binary(await getContent[ext](rows, tdoc), `${tdoc.title}.${ext}`); + this.binary(await getContent[ext](rows, this.tdoc), `${this.tdoc.title}.${ext}`); } } @@ -237,6 +233,7 @@ export class ContestEditHandler extends Handler { if (tid) { this.tdoc = await contest.get(domainId, tid); if (!this.tdoc) throw new ContestNotFoundError(domainId, tid); + if (contest.RULES[this.tdoc.rule].hidden) throw new ContestNotFoundError(domainId, tid); if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST); else this.checkPerm(PERM.PERM_EDIT_CONTEST_SELF); } else this.checkPerm(PERM.PERM_CREATE_CONTEST); @@ -372,17 +369,15 @@ export class ContestCodeHandler extends Handler { } } -export class ContestFilesHandler extends ContestDetailHandler { +export class ContestFilesHandler extends ContestDetailBaseHandler { @param('tid', Types.ObjectID) @param('pjax', Types.Boolean) - // @ts-ignore async get(domainId: string, tid: ObjectID, pjax = false) { - const tdoc = await contest.get(domainId, tid); - if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST); + if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST); const body = { - tdoc, - owner_udoc: await user.getById(domainId, tdoc.owner), - files: sortFiles(tdoc.files || []), + tdoc: this.tdoc, + owner_udoc: await user.getById(domainId, this.tdoc.owner), + files: sortFiles(this.tdoc.files || []), urlForFile: (filename: string) => this.url('contest_file_download', { tid, filename }), }; if (pjax) { @@ -407,14 +402,13 @@ export class ContestFilesHandler extends ContestDetailHandler { @param('tid', Types.ObjectID) @post('filename', Types.Name, true) async postUploadFile(domainId: string, tid: ObjectID, filename: string) { - const tdoc = await contest.get(domainId, tid); - if ((tdoc.files?.length || 0) >= system.get('limit.contest_files')) { + if ((this.tdoc.files?.length || 0) >= system.get('limit.contest_files')) { throw new ForbiddenError('File limit exceeded.'); } const file = this.request.files?.file; if (!file) throw new ValidationError('file'); const f = statSync(file.filepath); - const size = Math.sum((tdoc.files || []).map((i) => i.size)) + f.size; + const size = Math.sum((this.tdoc.files || []).map((i) => i.size)) + f.size; if (size >= system.get('limit.contest_files_size')) { throw new ForbiddenError('File size limit exceeded.'); } @@ -424,27 +418,25 @@ export class ContestFilesHandler extends ContestDetailHandler { const meta = await storage.getMeta(`contest/${domainId}/${tid}/${filename}`); const payload = { name: filename, ...pick(meta, ['size', 'lastModified', 'etag']) }; if (!meta) throw new Error('Upload failed'); - await contest.edit(domainId, tid, { files: [...(tdoc.files || []), payload] }); + await contest.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[]) { - const tdoc = await contest.get(domainId, tid); await Promise.all([ storage.del(files.map((t) => `contest/${domainId}/${tid}/${t}`), this.user._id), - contest.edit(domainId, tid, { files: tdoc.files.filter((i) => !files.includes(i.name)) }), + contest.edit(domainId, tid, { files: this.tdoc.files.filter((i) => !files.includes(i.name)) }), ]); this.back(); } } -export class ContestFileDownloadHandler extends ContestDetailHandler { +export class ContestFileDownloadHandler extends ContestDetailBaseHandler { @param('tid', Types.ObjectID) @param('filename', Types.Name) @param('noDisposition', Types.Boolean) - // @ts-ignore async get(domainId: string, tid: ObjectID, filename: string, noDisposition = false) { this.response.addHeader('Cache-Control', 'public'); const target = `contest/${domainId}/${tid}/${filename}`; @@ -466,7 +458,7 @@ export async function apply() { Route('contest_broadcast', '/contest/:tid/broadcast', ContestBroadcastHandler); Route('contest_edit', '/contest/:tid/edit', ContestEditHandler, PERM.PERM_VIEW_CONTEST); Route('contest_scoreboard', '/contest/:tid/scoreboard', ContestScoreboardHandler, PERM.PERM_VIEW_CONTEST); - Route('contest_scoreboard_download', '/contest/:tid/export/:ext', ContestScoreboardDownloadHandler, PERM.PERM_VIEW_CONTEST); + Route('contest_scoreboard_download', '/contest/:tid/export/:ext', ContestScoreboardHandler, PERM.PERM_VIEW_CONTEST); Route('contest_code', '/contest/:tid/code', ContestCodeHandler, PERM.PERM_VIEW_CONTEST); Route('contest_files', '/contest/:tid/file', ContestFilesHandler, PERM.PERM_VIEW_CONTEST); Route('contest_file_download', '/contest/:tid/file/:filename', ContestFileDownloadHandler, PERM.PERM_VIEW_CONTEST); diff --git a/packages/hydrooj/src/handler/homework.ts b/packages/hydrooj/src/handler/homework.ts index 08e5349b7..3e5f12a93 100644 --- a/packages/hydrooj/src/handler/homework.ts +++ b/packages/hydrooj/src/handler/homework.ts @@ -1,4 +1,3 @@ -import AdmZip from 'adm-zip'; import yaml from 'js-yaml'; import moment from 'moment-timezone'; import { ObjectID } from 'mongodb'; @@ -9,7 +8,7 @@ import { } from '../error'; import { PenaltyRules } from '../interface'; import paginate from '../lib/paginate'; -import { PERM, PRIV } from '../model/builtin'; +import { PERM } from '../model/builtin'; import * as contest from '../model/contest'; import * as discussion from '../model/discussion'; import problem from '../model/problem'; @@ -19,6 +18,7 @@ import user from '../model/user'; import { Handler, param, Route, Types, } from '../service/server'; +import { ContestCodeHandler, ContestScoreboardHandler } from './contest'; const validatePenaltyRules = (input: string) => yaml.load(input); const convertPenaltyRules = validatePenaltyRules; @@ -207,80 +207,16 @@ class HomeworkEditHandler extends Handler { } } -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, , nPages] = await contest.getScoreboard.call( - this, domainId, tid, false, page, - ); - const pdict = await problem.getList(domainId, tdoc.pids, true, undefined, false, [ - // Problem statistics display is allowed as we can view submission info in scoreboard. - ...problem.PROJECTION_CONTEST_LIST, 'nSubmit', 'nAccept', - ]); - const path = [ - ['Hydro', 'homepage'], - ['homework_main', 'homework_main'], - [tdoc.title, 'homework_detail', { tid }, true], - ['homework_scoreboard', null], - ]; - this.response.template = 'contest_scoreboard.html'; - this.response.body = { - tdoc, rows, path, udict, pdict, page, nPages, page_name: 'homework_scoreboard', - }; - } -} - -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', null, 'Unknown file extension'); - const [tdoc, rows] = await contest.getScoreboard.call(this, domainId, tid, true, 0); - this.binary(await getContent[ext](rows), `${tdoc.title}.${ext}`); - } -} - -class HomeworkCodeHandler extends Handler { - @param('tid', Types.ObjectID) - async get(domainId: string, tid: ObjectID) { - await this.limitRate('contest_code', 3600, 60); - const [tdoc, tsdocs] = await contest.getAndListStatus(domainId, tid); - if (!this.user.own(tdoc) && !this.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE)) { - this.checkPerm(PERM.PERM_READ_RECORD_CODE); - } - const rnames = {}; - for (const tsdoc of tsdocs) { - for (const pid in tsdoc.detail || {}) { - rnames[tsdoc.detail[pid].rid] = `U${tsdoc.uid}_P${pid}_R${tsdoc.detail[pid].rid}`; - } - } - const zip = new AdmZip(); - const rdocs = await record.getMulti(domainId, { - _id: { $in: Array.from(Object.keys(rnames)).map((id) => new ObjectID(id)) }, - }).toArray(); - for (const rdoc of rdocs) { - zip.addFile(`${rnames[rdoc._id.toHexString()]}.${rdoc.lang}`, Buffer.from(rdoc.code)); - } - this.binary(zip.toBuffer(), `${tdoc.title}.zip`); - } -} - export async function apply() { Route('homework_main', '/homework', HomeworkMainHandler, PERM.PERM_VIEW_HOMEWORK); Route('homework_create', '/homework/create', HomeworkEditHandler); Route('homework_detail', '/homework/:tid', HomeworkDetailHandler, PERM.PERM_VIEW_HOMEWORK); - Route('homework_scoreboard', '/homework/:tid/scoreboard', HomeworkScoreboardHandler, PERM.PERM_VIEW_HOMEWORK_SCOREBOARD); + Route('homework_scoreboard', '/homework/:tid/scoreboard', ContestScoreboardHandler, PERM.PERM_VIEW_HOMEWORK_SCOREBOARD); Route( 'homework_scoreboard_download', '/homework/:tid/scoreboard/download/:ext', - HomeworkScoreboardDownloadHandler, PERM.PERM_VIEW_HOMEWORK_SCOREBOARD, + ContestScoreboardHandler, PERM.PERM_VIEW_HOMEWORK_SCOREBOARD, ); - Route('homework_code', '/homework/:tid/code', HomeworkCodeHandler, PERM.PERM_VIEW_HOMEWORK); + Route('homework_code', '/homework/:tid/code', ContestCodeHandler, PERM.PERM_VIEW_HOMEWORK); Route('homework_edit', '/homework/:tid/edit', HomeworkEditHandler); } diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index 2b6d18b17..e800dfc95 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -13,8 +13,7 @@ import { ValidationError, } from '../error'; import { - ProblemDoc, ProblemStatusDoc, Tdoc, - User, + ProblemDoc, ProblemStatusDoc, User, } from '../interface'; import paginate from '../lib/paginate'; import { isPid, parsePid as convertPid } from '../lib/validator'; @@ -35,6 +34,7 @@ import { Handler, param, post, query, Route, route, Types, } from '../service/server'; import { registerResolver, registerValue } from './api'; +import { ContestDetailBaseHandler } from './contest'; export const parseCategory = (value: string) => value.replace(/,/g, ',').split(',').map((e) => e.trim()); export const parsePid = (value: string) => (isSafeInteger(value) ? +value : value); @@ -112,30 +112,7 @@ function buildQuery(udoc: User) { return q; } -export class ProblemHandler extends Handler { - async cleanup() { - if (this.response.template === 'problem_main.html' && this.request.json) { - const { - page, pcount, pcountRelation, ppcount, pdocs, psdict, category, qs, - } = this.response.body; - this.response.body = { - title: this.renderTitle(this.translate('problem_main')), - fragments: (await Promise.all([ - this.renderHTML('partials/problem_list.html', { - page, ppcount, pcount, pdocs, psdict, qs, - }), - this.renderHTML('partials/problem_stat.html', { pcount, pcountRelation }), - this.renderHTML('partials/problem_lucky.html', { qs }), - ])).map((i) => ({ html: i })), - raw: { - page, pcount, ppcount, pdocs, psdict, category, - }, - }; - } - } -} - -export class ProblemMainHandler extends ProblemHandler { +export class ProblemMainHandler extends Handler { @param('page', Types.PositiveInt, true) @param('q', Types.Content, true) async get(domainId: string, page = 1, q = '') { @@ -204,16 +181,32 @@ export class ProblemMainHandler extends ProblemHandler { ).then((res) => Object.assign(psdict, res))), ); } - this.response.body = { - page, - pcount, - ppcount, - pcountRelation, - pdocs, - psdict, - category: category.join(','), - qs: q, - }; + if (this.request.json) { + this.response.body = { + title: this.renderTitle(this.translate('problem_main')), + fragments: (await Promise.all([ + this.renderHTML('partials/problem_list.html', { + page, ppcount, pcount, pdocs, psdict, qs: q, + }), + this.renderHTML('partials/problem_stat.html', { pcount, pcountRelation }), + this.renderHTML('partials/problem_lucky.html', { qs: q }), + ])).map((i) => ({ html: i })), + raw: { + page, pcount, ppcount, pdocs, psdict, category, + }, + }; + } else { + this.response.body = { + page, + pcount, + ppcount, + pcountRelation, + pdocs, + psdict, + category: category.join(','), + qs: q, + }; + } } @param('pid', Types.UnsignedInt) @@ -283,7 +276,7 @@ export class ProblemMainHandler extends ProblemHandler { } } -export class ProblemRandomHandler extends ProblemHandler { +export class ProblemRandomHandler extends Handler { @param('q', Types.Content, true) async get(domainId: string, qs = '') { const category = flattenDeep(decodeURIComponent(qs).split(' ') @@ -299,10 +292,8 @@ export class ProblemRandomHandler extends ProblemHandler { } } -export class ProblemDetailHandler extends ProblemHandler { +export class ProblemDetailHandler extends ContestDetailBaseHandler { pdoc: ProblemDoc; - tdoc?: Tdoc<30>; - tsdoc?: any; udoc: User; psdoc: ProblemStatusDoc; @@ -312,9 +303,7 @@ export class ProblemDetailHandler extends ProblemHandler { this.pdoc = await problem.get(domainId, pid); if (!this.pdoc) throw new ProblemNotFoundError(domainId, pid); if (tid) { - this.tdoc = await contest.get(domainId, tid); if (!this.tdoc?.pids?.includes(this.pdoc.docId)) throw new ContestNotFoundError(domainId, tid); - this.tsdoc = await contest.getStatus(domainId, tid, this.user._id); if (contest.isNotStarted(this.tdoc)) throw new ContestNotLiveError(tid); if (!contest.isDone(this.tdoc, this.tsdoc) && (!this.tsdoc?.attend || !this.tsdoc.startAt)) throw new ContestNotAttendedError(tid); this.pdoc.tag.length = 0; diff --git a/packages/hydrooj/src/model/document.ts b/packages/hydrooj/src/model/document.ts index 023049e28..e8623e3ef 100644 --- a/packages/hydrooj/src/model/document.ts +++ b/packages/hydrooj/src/model/document.ts @@ -26,6 +26,7 @@ export const TYPE_DISCUSSION: 21 = 21; export const TYPE_DISCUSSION_REPLY: 22 = 22; export const TYPE_CONTEST: 30 = 30; export const TYPE_TRAINING: 40 = 40; +/** @deprecated use `TYPE_CONTEST` with rule `homework` instead. */ export const TYPE_HOMEWORK: 60 = 60; export const TYPE_BLOG: 70 = 70; diff --git a/packages/hydrooj/src/service/server.ts b/packages/hydrooj/src/service/server.ts index a4a6056a0..654cc0fad 100644 --- a/packages/hydrooj/src/service/server.ts +++ b/packages/hydrooj/src/service/server.ts @@ -291,6 +291,7 @@ async function handle(ctx: KoaContext, HandlerClass, checker) { await bail(`handler/before-prepare/${HandlerClass.name.replace(/Handler$/, '')}`, h); await bail('handler/before-prepare', h); h.args.__prepare = Date.now(); + if (h.__prepare) await h.__prepare(args); if (h._prepare) await h._prepare(args); if (h.prepare) await h.prepare(args); h.args.__prepareDone = Date.now(); diff --git a/packages/ui-default/locales/zh.yaml b/packages/ui-default/locales/zh.yaml index e6da2b237..671d4372b 100644 --- a/packages/ui-default/locales/zh.yaml +++ b/packages/ui-default/locales/zh.yaml @@ -782,6 +782,7 @@ Testdata Config: 测试数据配置 Testdata: 测试数据 Text: 文本 The 'default' role applies to ALL REGISTERED USER.: default 角色作用于所有已注册用户 +The contest is a flexible time contest. You need to complete the contest within a specified time after you attended.: 本场比赛采用灵活时间模式,你需要在参加后的指定时间内完成比赛。 The homework's deadline is due but in extension. You can still submit for this problem but your score will be penalized.: 作业已超过截止时间,但仍在延期时间内。您递交题目将无法获得全部分数。 The invitation code to enter to successfully join the domain. You can only use letters and numbers in the code and it should not be longer than 64 characters.: 加入此域的邀请码。您只能使用字母和数字,并且不能长于 64 个字符。 The invitation code you provided is invalid.: 您提供的邀请码无效。 diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index eeef9679f..adaaceedb 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.38.11", + "version": "4.38.12", "author": "undefined ", "license": "AGPL-3.0", "main": "hydro.js", @@ -10,7 +10,7 @@ "lint": "eslint" }, "devDependencies": { - "@blueprintjs/core": "^4.5.1", + "@blueprintjs/core": "^4.6.0", "@fontsource/dm-mono": "^4.5.8", "@fontsource/fira-code": "^4.5.10", "@fontsource/inconsolata": "^4.5.6", @@ -27,7 +27,7 @@ "@types/markdown-it": "^12.2.3", "@types/pickadate": "^3.5.32", "@types/qrcode": "^1.4.2", - "@types/react-dom": "^18.0.5", + "@types/react-dom": "^18.0.6", "@types/redux-logger": "^3.0.9", "ansi_up": "^5.1.0", "autoprefixer": "^9.8.8", @@ -61,7 +61,7 @@ "jquery.transit": "^0.9.12", "matchmedia-polyfill": "^0.3.2", "mini-css-extract-plugin": "^1.6.2", - "moment": "^2.29.3", + "moment": "^2.29.4", "monaco-editor": "^0.33.0", "monaco-editor-nls": "^2.0.0", "monaco-editor-webpack-plugin": "^7.0.1", diff --git a/packages/ui-default/templates/contest_detail.html b/packages/ui-default/templates/contest_detail.html index f399c5032..9710c496e 100644 --- a/packages/ui-default/templates/contest_detail.html +++ b/packages/ui-default/templates/contest_detail.html @@ -30,6 +30,11 @@

{{ _('We will check code similarity after this contest.') }}

{% endif %} + {% if tdoc.duration %} +
+

{{ _('The contest is a flexible time contest. You need to complete the contest within a specified time after you attended.') }}

+
+ {% endif %} {% if model.contest.isOngoing(tdoc, tsdoc) or model.contest.isDone(tdoc, tsdoc) %} diff --git a/packages/ui-default/templates/contest_scoreboard.html b/packages/ui-default/templates/contest_scoreboard.html index f324b9624..1024708fa 100644 --- a/packages/ui-default/templates/contest_scoreboard.html +++ b/packages/ui-default/templates/contest_scoreboard.html @@ -1,6 +1,6 @@ {% import "components/paginator.html" as paginator with context %} {% extends "layout/basic.html" %} -{% set type = 'homework' if tdoc.docType == 60 else 'contest' %} +{% set type = 'homework' if tdoc.rule == 'homework' else 'contest' %} {% block content %}
diff --git a/packages/ui-default/templates/partials/contest_sidebar.html b/packages/ui-default/templates/partials/contest_sidebar.html index 06417febb..578085577 100644 --- a/packages/ui-default/templates/partials/contest_sidebar.html +++ b/packages/ui-default/templates/partials/contest_sidebar.html @@ -109,7 +109,7 @@
{{ _('Rule') }}
{{ model.contest.RULES[tdoc.rule].TEXT }}
{{ _('Problem') }}
{{ tdoc.pids.length }}
{{ _('Start at') }}
{{ contest.render_time(tsdoc.startAt or tdoc.beginAt) }}
-
{{ _('End at') }}
{{ contest.render_time(tdoc.endAt) }}
+
{{ _('End at') }}
{{ contest.render_time(tsdoc.endAt or tdoc.endAt) }}
{{ _('Duration') }}
{{ contest.render_duration(tdoc) }} {{ _('hour(s)') }}
{{ _('Host') }}
{{ user.render_inline(owner_udoc, badge=false) }}
{{ _('Partic.') }}
{{ tdoc['attend']|default(0) }}
diff --git a/packages/utils/package.json b/packages/utils/package.json index ed861ae24..a2060ee43 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -12,7 +12,7 @@ "js-yaml": "^4.1.0", "moment-timezone": "^0.5.34", "mongodb": "^3.7.3", - "systeminformation": "^5.11.22" + "systeminformation": "^5.11.24" }, "devDependencies": { "@types/fs-extra": "^9.0.13" diff --git a/packages/vjudge/package.json b/packages/vjudge/package.json index e06ed4417..e8537ed75 100644 --- a/packages/vjudge/package.json +++ b/packages/vjudge/package.json @@ -12,10 +12,10 @@ "chrome-finder": "^1.0.7", "jsdom": "^19.0.0", "lodash": "^4.17.21", - "puppeteer-core": "^15.1.1", + "puppeteer-core": "^15.3.2", "puppeteer-extra": "3.3.0", - "puppeteer-extra-plugin-portal": "^3.1.0", - "puppeteer-extra-plugin-stealth": "^2.10.1", + "puppeteer-extra-plugin-portal": "3.1.0", + "puppeteer-extra-plugin-stealth": "2.10.1", "superagent": "^7.1.5", "superagent-proxy": "^3.0.0" },