From 1722fbfaca41520dd6d55478cca0399190b54307 Mon Sep 17 00:00:00 2001 From: undefined Date: Sun, 12 Jul 2020 13:36:12 +0800 Subject: [PATCH] =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E8=AE=BE=E7=BD=AE=E4=B8=AD?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89UI=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/launch.json | 21 ++++ .vscode/tasks.json | 13 +++ hydro/handler/contest.ts | 2 +- hydro/handler/home.ts | 7 +- hydro/handler/homework.ts | 16 ++- hydro/handler/judge.ts | 2 +- hydro/handler/manage.ts | 1 - hydro/handler/misc.ts | 14 +++ hydro/handler/user.ts | 7 +- hydro/interface.ts | 105 ++++++++++++++++--- hydro/model/contest.ts | 164 ++++++++++++++---------------- hydro/model/document.ts | 5 +- hydro/model/problem.ts | 4 +- hydro/model/record.ts | 11 +- hydro/model/setting.ts | 3 + hydro/model/user.ts | 26 +++-- hydro/script/install.ts | 2 +- hydro/service/server.ts | 2 +- templates/contest_scoreboard.html | 8 +- templates/extra.css | 10 ++ templates/layout/html5.html | 1 + 21 files changed, 284 insertions(+), 140 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 templates/extra.css diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..394e1164a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // 使用 IntelliSense 了解相关属性。 + // 悬停以查看现有属性的描述。 + // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/.build/app.js", + "preLaunchTask": "npm: build", + "outFiles": [ + "${workspaceFolder}/dist/**/*.js" + ] + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..bfaf41478 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,13 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "build", + "group": "build", + "problemMatcher": [], + "label": "npm: build", + "detail": "node build --development" + } + ] +} \ No newline at end of file diff --git a/hydro/handler/contest.ts b/hydro/handler/contest.ts index 2ca57b119..8af07c695 100644 --- a/hydro/handler/contest.ts +++ b/hydro/handler/contest.ts @@ -200,7 +200,7 @@ class ContestEditHandler extends ContestHandler { class ContestProblemHandler extends ContestHandler { @param('tid', Types.ObjectID) @param('pid', Types.UnsignedInt) - async prepare(domainId: string, tid: ObjectID, pid: number) { + async _prepare(domainId: string, tid: ObjectID, pid: number) { [this.tdoc, this.pdoc] = await Promise.all([ contest.get(domainId, tid), problem.get(domainId, pid, this.user._id), diff --git a/hydro/handler/home.ts b/hydro/handler/home.ts index 9988206dd..a1d85e8a1 100644 --- a/hydro/handler/home.ts +++ b/hydro/handler/home.ts @@ -1,6 +1,7 @@ import { VerifyPasswordError, UserAlreadyExistError, InvalidTokenError, NotFoundError, + UserNotFoundError, } from '../error'; import * as bus from '../service/bus'; import { @@ -141,7 +142,7 @@ class HomeSecurityHandler extends Handler { async postChangeMail({ domainId, currentPassword, mail }) { this.limitRate('send_mail', 3600, 30); this.user.checkPassword(currentPassword); - const udoc = await user.getByEmail(domainId, mail, true); + const udoc = await user.getByEmail(domainId, mail); if (udoc) throw new UserAlreadyExistError(mail); const [code] = await token.add( token.TYPE_CHANGEMAIL, @@ -212,7 +213,7 @@ class UserChangemailWithCodeHandler extends Handler { if (!tdoc || tdoc.uid !== this.user._id) { throw new InvalidTokenError(code); } - const udoc = await user.getByEmail(domainId, tdoc.mail, true); + const udoc = await user.getByEmail(domainId, tdoc.mail); if (udoc) throw new UserAlreadyExistError(tdoc.mail); await Promise.all([ user.setEmail(this.user._id, tdoc.mail), @@ -253,6 +254,7 @@ class HomeMessagesHandler extends Handler { async postSend({ uid, content }) { const udoc = await user.getById('system', uid); + if (!udoc) throw new UserNotFoundError(uid); const mdoc = await message.send(this.user._id, uid, content); // TODO(twd2): improve here: projection if (this.user._id !== uid) { @@ -262,6 +264,7 @@ class HomeMessagesHandler extends Handler { } async postDeleteMessage({ messageId }) { + // TODO limitations await message.del(messageId); this.back(); } diff --git a/hydro/handler/homework.ts b/hydro/handler/homework.ts index ce51b8588..9e078373d 100644 --- a/hydro/handler/homework.ts +++ b/hydro/handler/homework.ts @@ -7,6 +7,7 @@ import { ValidationError, HomeworkNotLiveError, ProblemNotFoundError, HomeworkNotAttendedError, } from '../error'; +import { PenaltyRules } from '../interface'; import { Route, Handler, Types, param, } from '../service/server'; @@ -21,6 +22,12 @@ import * as document from '../model/document'; import paginate from '../lib/paginate'; import { isTitle, isContent } from '../lib/validator'; +const validatePenaltyRules = (input: string) => { + const s = yaml.safeLoad(input); + return s; +}; +const convertPenaltyRules = validatePenaltyRules; + const HomeworkHandler = contest.ContestHandlerMixin(Handler); class HomeworkMainHandler extends HomeworkHandler { @@ -28,7 +35,7 @@ class HomeworkMainHandler extends HomeworkHandler { const tdocs = await contest.getMulti(domainId, {}, document.TYPE_HOMEWORK).toArray(); const calendar = []; for (const tdoc of tdocs) { - const cal = { ...tdoc, url: `/homework/${tdoc.docId}` }; + const cal = { ...tdoc, url: this.url('homework_detail', { tid: tdoc.docId }) }; if (contest.isExtended(tdoc) || contest.isDone(tdoc)) { cal.endAt = tdoc.endAt; cal.penaltySince = tdoc.penaltySince; @@ -211,7 +218,7 @@ class HomeworkCreateHandler extends HomeworkHandler { @param('penaltySinceDate', Types.Date) @param('penaltySinceTime', Types.Time) @param('extensionDays', Types.Float) - @param('penaltyRules', Types.String) + @param('penaltyRules', Types.String, validatePenaltyRules, convertPenaltyRules) @param('title', Types.String, isTitle) @param('content', Types.String, isContent) @param('pids', Types.String) @@ -219,7 +226,7 @@ class HomeworkCreateHandler extends HomeworkHandler { async post( domainId: string, beginAtDate: string, beginAtTime: string, penaltySinceDate: string, penaltySinceTime: string, extensionDays: number, - penaltyRules: string, title: string, content: string, _pids: string, rated = false, + penaltyRules: PenaltyRules, title: string, content: string, _pids: string, rated = false, ) { const pids = _pids.split(',').map((i) => { if (isSafeInteger(parseInt(i, 10))) return parseInt(i, 10); @@ -278,7 +285,7 @@ class HomeworkEditHandler extends HomeworkHandler { @param('penaltySinceDate', Types.Date) @param('penaltySinceTime', Types.Time) @param('extensionDays', Types.Float) - @param('penaltyRules', Types.String) + @param('penaltyRules', Types.String, validatePenaltyRules, convertPenaltyRules) @param('title', Types.String, isTitle) @param('content', Types.String, isContent) @param('pids', Types.String) @@ -330,6 +337,7 @@ class HomeworkEditHandler extends HomeworkHandler { class HomeworkScoreboardHandler extends HomeworkHandler { @param('tid', Types.ObjectID) async get(domainId: string, tid: ObjectID) { + console.log(123); const [tdoc, rows, udict] = await this.getScoreboard( domainId, tid, false, document.TYPE_HOMEWORK, ); diff --git a/hydro/handler/judge.ts b/hydro/handler/judge.ts index a66051135..eea36bc97 100644 --- a/hydro/handler/judge.ts +++ b/hydro/handler/judge.ts @@ -168,7 +168,7 @@ class JudgeConnectionHandler extends ConnectionHandler { async cleanup() { if (this.processing) { - await record.reset(this.processing.domainId, this.processing.rid); + await record.reset(this.processing.domainId, this.processing.rid, false); task.add(this.processing); } } diff --git a/hydro/handler/manage.ts b/hydro/handler/manage.ts index c95f50280..1f2c319a8 100644 --- a/hydro/handler/manage.ts +++ b/hydro/handler/manage.ts @@ -169,7 +169,6 @@ class SystemSettingHandler extends SystemHandler { for (const key in args) { if (typeof args[key] === 'object') { for (const subkey in args[key]) { - console.log(`${key}.${subkey}`, set(`${key}.${subkey}`, args[key][subkey])); if (typeof set(`${key}.${subkey}`, args[key][subkey]) !== 'undefined') { tasks.push(system.set(`${key}.${subkey}`, set(`${key}.${subkey}`, args[key][subkey]))); } diff --git a/hydro/handler/misc.ts b/hydro/handler/misc.ts index f8c7c0981..e6f0dd2de 100644 --- a/hydro/handler/misc.ts +++ b/hydro/handler/misc.ts @@ -1,5 +1,6 @@ import { ObjectID } from 'mongodb'; import { PRIV } from '../model/builtin'; +import * as system from '../model/system'; import * as file from '../model/file'; import * as user from '../model/user'; import * as markdown from '../lib/markdown'; @@ -77,6 +78,18 @@ class SockToken extends Handler { } } +class UiSettingsHandler extends Handler { + async get() { + const [ + header, nav, headerBackground, + ] = await system.getMany([ + 'ui.header', 'ui.nav', 'ui.headerBackground', + ]); + this.response.body = await this.renderHTML('extra.css', { header, nav, headerBackground }); + this.response.type = 'text/css'; + } +} + export async function apply() { Route('file_download', '/fs/:fileId/:secret', FileDownloadHandler); Route('file_download_with_name', '/fs/:fileId/:name/:secret', FileDownloadHandler); @@ -85,6 +98,7 @@ export async function apply() { Route('switch_language', '/language/:lang', SwitchLanguageHandler); Route('markdown', '/markdown', MarkdownHandler); Route('token', '/token', SockToken); + Route('ui', '/extra.css', UiSettingsHandler); } apply.updateStatus = function updateStatus(args) { diff --git a/hydro/handler/user.ts b/hydro/handler/user.ts index aa239e045..dfd3d986e 100644 --- a/hydro/handler/user.ts +++ b/hydro/handler/user.ts @@ -62,7 +62,7 @@ class UserRegisterHandler extends Handler { @param('mail', Types.String, isEmail) async post(domainId: string, mail: string) { - if (await user.getByEmail('system', mail, true)) throw new UserAlreadyExistError(mail); + if (await user.getByEmail('system', mail)) throw new UserAlreadyExistError(mail); this.limitRate('send_mail', 3600, 30); const t = await token.add( token.TYPE_REGISTRATION, @@ -158,7 +158,8 @@ class UserDetailHandler extends Handler { @param('uid', Types.Int) async get(domainId: string, uid: number) { const isSelfProfile = this.user._id === uid; - const udoc = await user.getById(domainId, uid, true); + const udoc = await user.getById(domainId, uid); + if (!udoc) throw new UserNotFoundError(uid); const sdoc = await token.getMostRecentSessionByUid(uid); const rdocs = await record.getByUid(domainId, uid); const pdict = await problem.getList( @@ -310,7 +311,7 @@ class OauthCallbackHandler extends Handler { if (args.type === 'github') r = await this.github(args); else if (args.type === 'google') r = await this.google(args); else throw new UserFacingError('Oauth type'); - const udoc = await user.getByEmail('system', r.email, true); + const udoc = await user.getByEmail('system', r.email); if (udoc) { this.session.uid = udoc._id; } else { diff --git a/hydro/interface.ts b/hydro/interface.ts index 08cd85626..45c60a0a2 100644 --- a/hydro/interface.ts +++ b/hydro/interface.ts @@ -13,21 +13,51 @@ export interface Setting { flag: number, } +export interface Udoc { + udoc: () => any, + dudoc: () => any, + _id: number, + mail: string, + uname: string, + salt: () => string, + hash: () => string, + hashType: string, + priv: number, + regat: Date, + loginat: Date, + perm: string, + role: string, + regip: () => string, + loginip: () => string, + hasPerm: (perm: string) => boolean, + hasPriv: (priv: number) => boolean, + checkPassword: (password: string) => void, + [key: string]: any, +} + +export interface Udict { + [key: number]: Udoc, +} + export interface Pdoc { - _id: ObjectID - domainId: string - docId: number - pid: string - owner: number - title: string - content: string - nSubmit: number - nAccept: number - tag: string[] + _id: ObjectID, + domainId: string, + docId: number, + pid: string, + owner: number, + title: string, + content: string, + nSubmit: number, + nAccept: number, + tag: string[], category: string[], - data: ObjectID | null - hidden: boolean - config: string + data?: ObjectID, + hidden: boolean, + config: string, +} + +export interface Pdict { + [key: string]: Pdoc, } export interface TestCase { @@ -63,6 +93,55 @@ export interface Rdoc { ttype?: number, } +export interface ScoreboardNode { + type: string, + value: string, + raw?: any, +} + +export interface PenaltyRules { + [key: string]: number, +} + +export interface Tdoc { + _id: ObjectID, + domainId: string, + docId: ObjectID, + docType: number, + beginAt: Date, + endAt: Date, + penaltySince?: Date, + penaltyRules?: PenaltyRules, + attend: number, + title: string, + content: string, + rule: string, + pids: number[], +} + +interface ContestStat { + detail: any, + [key: string]: any, +} + +export interface ContestRule { + TEXT: string, + check: (args: any) => any, + statusSort: any, + showScoreboard: (tdoc: Tdoc, now: Date) => boolean, + showRecord: (tdoc: Tdoc, now: Date) => boolean, + stat: (tdoc: Tdoc, journal: any[]) => ContestStat, + scoreboard: ( + isExport: boolean, _: (s: string) => string, + tdoc: Tdoc, rankedTsdocs: any[] | Generator, udict: Udict, pdict: Pdict + ) => ScoreboardNode[][], + rank: (tsdocs: any[]) => any[] | Generator, +} + +export interface ContestRules { + [key: string]: ContestRule, +} + export type ProblemImporter = (url: string, handler: any) => Promise<[Pdoc, fs.ReadStream?]> | [Pdoc, fs.ReadStream?]; diff --git a/hydro/model/contest.ts b/hydro/model/contest.ts index ae9894377..72e9b49aa 100644 --- a/hydro/model/contest.ts +++ b/hydro/model/contest.ts @@ -7,34 +7,18 @@ import { } from '../error'; import * as document from './document'; import { PERM } from './builtin'; +import { + Tdoc, ContestRule, ContestRules, + Udict, ScoreboardNode, +} from '../interface'; import * as validator from '../lib/validator'; import * as misc from '../lib/misc'; import ranked from '../lib/rank'; -interface Column { - type: string, - value: string, - raw?: any -} - -export interface Tdoc { - _id: ObjectID, - domainId: string, - docId: ObjectID, - docType: number, - beginAt: Date, - endAt: Date, - penaltySince?: Date, - attend: number, - title: string, - content: string, - rule: string, - pids: number[] -} - -const acm = { +const acm: ContestRule = { TEXT: 'ACM/ICPC', check: () => { }, + statusSort: { accept: -1, time: 1 }, showScoreboard: () => true, showRecord: (tdoc, now) => now > tdoc.endAt, stat: (tdoc, journal: any[]) => { @@ -47,17 +31,18 @@ const acm = { if (tdoc.pids.includes(j.pid) && !(effective[j.pid] && effective[j.pid].accept)) { effective[j.pid] = j; + if (!j.accept) naccept[j.pid] = (naccept[j.pid] || 0) + 1; } - if (!j.accept) naccept[j.pid]++; } function _time(jdoc) { - const real = jdoc.rid.generationTime - Math.floor(tdoc.beginAt / 1000); + const real = jdoc.rid.generationTime - Math.floor(tdoc.beginAt.getTime() / 1000); const penalty = 20 * 60 * naccept[jdoc.pid]; return real + penalty; } - for (const j in effective) { + for (const key in effective) { + const j = effective[key]; detail.push({ - ...effective[j], naccept: naccept[effective[j].pid], time: _time(effective[j]), + ...j, naccept: naccept[j.pid], time: _time(j), }); } for (const d of detail) { @@ -66,8 +51,8 @@ const acm = { } return { accept, time, detail }; }, - scoreboard(isExport: boolean, _: (s: string) => string, tdoc, rankedTsdocs, udict, pdict) { - const columns: Column[] = [ + scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) { + const columns: ScoreboardNode[] = [ { type: 'rank', value: _('Rank') }, { type: 'user', value: _('User') }, { type: 'solved_problems', value: _('Solved Problems') }, @@ -78,40 +63,41 @@ const acm = { { type: 'total_time_str', value: _('Total Time') }, ); } - for (const i in tdoc.pids) { + for (let i = 1; i <= tdoc.pids.length; i++) { + const pid = tdoc.pids[i - 1]; if (isExport) { columns.push( { type: 'problem_flag', - value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title), + value: '#{0} {1}'.format(i, pdict[pid].title), }, { type: 'problem_time', - value: '#{0} {1}'.format(i + 1, _('Time (Seconds)')), + value: '#{0} {1}'.format(i, _('Time (Seconds)')), }, { type: 'problem_time_str', - value: '#{0} {1}'.format(i + 1, _('Time')), + value: '#{0} {1}'.format(i, _('Time')), }, ); } else { columns.push({ type: 'problem_detail', - value: '#{0}'.format(i + 1), - raw: pdict[tdoc.pids[i]], + value: '#{0}'.format(i), + raw: pid, }); } } const rows = [columns]; for (const [rank, tsdoc] of rankedTsdocs) { const tsddict = {}; - if (tdoc.detail) { - for (const item of tsdoc.journal) tsddict[item.pid] = item; + if (tsdoc.detail) { + for (const item of tsdoc.detail) tsddict[item.pid] = item; } const row = []; row.push( { type: 'string', value: rank }, - { type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] }, + { type: 'user', value: udict[tsdoc.uid].uname, raw: tsdoc.uid }, { type: 'string', value: tsdoc.accept || 0 }, ); if (isExport) { @@ -121,17 +107,17 @@ const acm = { ); } for (const pid of tdoc.pids) { - let rdoc; + let rid; let colAccepted; let colTime; let colTimeStr; - if ((tsddict[pid] || {}).accept) { - rdoc = tsddict[pid].rid; + if (tsddict[pid]?.accept) { + rid = tsddict[pid].rid; colAccepted = _('Accepted'); colTime = tsddict[pid].time; - colTimeStr = colTime; + colTimeStr = misc.formatSeconds(colTime); } else { - rdoc = null; + rid = null; colAccepted = '-'; colTime = '-'; colTimeStr = '-'; @@ -144,20 +130,21 @@ const acm = { row.push({ type: 'record', value: '{0}\n{1}'.format(colAccepted, colTimeStr), - raw: rdoc, + raw: rid, }); } - rows.push(row); } + rows.push(row); } return rows; }, - rank: (tdocs) => ranked(tdocs, (a, b) => a.score === b.score), + rank: (tsdocs) => ranked(tsdocs, (a, b) => a.score === b.score), }; -const oi = { +const oi: ContestRule = { TEXT: 'OI', check: () => { }, + statusSort: { score: -1 }, stat: (tdoc, journal) => { const detail = {}; let score = 0; @@ -176,50 +163,55 @@ const oi = { return now > tdoc.endAt; }, scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) { - const columns: Column[] = [ + const columns: ScoreboardNode[] = [ { type: 'rank', value: _('Rank') }, { type: 'user', value: _('User') }, { type: 'total_score', value: _('Total Score') }, ]; - for (const i in tdoc.pids) { + for (let i = 1; i <= tdoc.pids.length; i++) { if (isExport) { columns.push({ type: 'problem_score', - value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title), + value: '#{0} {1}'.format(i, pdict[tdoc.pids[i - 1]].title), }); } else { columns.push({ type: 'problem_detail', - value: '#{0}'.format(i + 1), - raw: pdict[tdoc.pids[i]], + value: '#{0}'.format(i), + raw: tdoc.pids[i - 1], }); } } const rows = [columns]; for (const [rank, tsdoc] of rankedTsdocs) { const tsddict = {}; - if (tsdoc.journal) { for (const item of tsdoc.journal) tsddict[item.pid] = item; } + if (tsdoc.journal) { + for (const item of tsdoc.journal) tsddict[item.pid] = item; + } const row = []; - row.push({ type: 'string', value: rank }); - row.push({ type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] }); - row.push({ type: 'string', value: tsdoc.score || 0 }); + row.push( + { type: 'string', value: rank }, + { type: 'user', value: udict[tsdoc.uid].uname, raw: tsdoc.uid }, + { type: 'string', value: tsdoc.score || 0 }, + ); for (const pid of tdoc.pids) { row.push({ type: 'record', - value: (tsddict[pid] || {}).score || '-', - raw: (tsddict[pid] || {}).rid || null, + value: tsddict[pid]?.score || '-', + raw: tsddict[pid]?.rid || null, }); } rows.push(row); } return rows; }, - rank: (tdocs) => ranked(tdocs, (a, b) => a.score === b.score), + rank: (tsdocs) => ranked(tsdocs, (a, b) => a.score === b.score), }; -const homework = { +const homework: ContestRule = { TEXT: 'Assignment', check: () => { }, + statusSort: { penaltyScore: -1, time: 1 }, stat: (tdoc, journal) => { const effective = {}; for (const j of journal) { @@ -233,6 +225,7 @@ const homework = { const real = jdoc.rid.generationTime - tdoc.beginAt.getTime() / 1000; return Math.floor(real); } + function penaltyScore(jdoc) { const { score } = jdoc; const exceedSeconds = Math.floor( @@ -266,7 +259,7 @@ const homework = { showScoreboard: () => true, showRecord: () => true, scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) { - const columns: Column[] = [ + const columns: ScoreboardNode[] = [ { type: 'rank', value: _('Rank') }, { type: 'user', value: _('User') }, { type: 'total_score', value: _('Score') }, @@ -278,32 +271,32 @@ const homework = { ); } columns.push({ type: 'total_time_str', value: _('Total Time') }); - for (const index in tdoc.pids) { - const pid = tdoc.pids[index]; + for (let i = 1; i <= tdoc.pids.length; i++) { + const pid = tdoc.pids[i - 1]; if (isExport) { columns.push( { type: 'problem_score', - value: '#{0} {1}'.format(index + 1, pdict[pid].title), + value: '#{0} {1}'.format(i, pdict[pid].title), }, { type: 'problem_original_score', - value: '#{0} {1}'.format(index + 1, _('Original Score')), + value: '#{0} {1}'.format(i, _('Original Score')), }, { type: 'problem_time', - value: '#{0} {1}'.format(index + 1, _('Time (Seconds)')), + value: '#{0} {1}'.format(i, _('Time (Seconds)')), }, { type: 'problem_time_str', - value: '#{0} {1}'.format(index + 1, _('Time')), + value: '#{0} {1}'.format(i, _('Time')), }, ); } else { columns.push({ type: 'problem_detail', - value: '#{0}'.format(index + 1), - raw: pdict[pid], + value: '#{0}'.format(i), + raw: pid, }); } } @@ -318,7 +311,7 @@ const homework = { { type: 'user', value: udict[tsdoc.uid].uname, - raw: udict[tsdoc.uid], + raw: tsdoc.uid, }, { type: 'string', @@ -331,10 +324,10 @@ const homework = { } row.push({ type: 'string', value: misc.formatSeconds(tsdoc.time || 0) }); for (const pid of tdoc.pids) { - const rdoc = (tsddict[pid] || {}).rid; - const colScore = (tsddict[pid] || {}).penaltyScore || '-'; - const colOriginalScore = (tsddict[pid] || {}).score || '-'; - const colTime = (tsddict[pid] || {}).time || '-'; + const rid = tsddict[pid]?.rid; + const colScore = tsddict[pid]?.penaltyScore || '-'; + const colOriginalScore = tsddict[pid]?.score || '-'; + const colTime = tsddict[pid]?.time || '-'; const colTimeStr = colTime !== '-' ? misc.formatSeconds(colTime || 0) : '-'; if (isExport) { row.push( @@ -347,7 +340,7 @@ const homework = { row.push({ type: 'record', value: '{0} / {1}\n{2}'.format(colScore, colOriginalScore, colTimeStr), - raw: rdoc, + raw: rid, }); } } @@ -358,10 +351,14 @@ const homework = { rank: (tsdocs) => ranked(tsdocs, (a, b) => a.score === b.score), }; -export const RULES = { +export const RULES: ContestRules = { acm, oi, homework, }; +function _getStatusJournal(tsdoc) { + return tsdoc.journal.sort((a, b) => (a.rid.generationTime - b.rid.generationTime)); +} + export function add( domainId: string, title: string, content: string, owner: number, rule: string, beginAt = new Date(), endAt = new Date(), pids = [], @@ -407,11 +404,14 @@ export async function updateStatus( domainId: string, tid: ObjectID, uid: number, rid: ObjectID, pid: number, accept = false, score = 0, type = document.TYPE_CONTEST, ) { - await get(domainId, tid, type); + const tdoc = await get(domainId, tid, type); const tsdoc = await document.revPushStatus(domainId, type, tid, uid, 'journal', { rid, pid, accept, score, }); if (!tsdoc.attend) throw new ContestNotAttendedError(tid, uid); + const journal = _getStatusJournal(tsdoc); + const stats = RULES[tdoc.rule].stat(tdoc, journal); + return await document.revSetStatus(domainId, type, tid, uid, tsdoc.rev, { journal, ...stats }); } export function getStatus( @@ -498,13 +498,11 @@ export const ContestHandlerMixin = (c) => class extends c { async getScoreboard( domainId: string, tid: ObjectID, isExport = false, docType = document.TYPE_CONTEST, - ) { + ): Promise<[Tdoc, ScoreboardNode[][], Udict]> { const tdoc = await get(domainId, tid, docType); if (!this.canShowScoreboard(tdoc)) throw new ContestScoreboardHiddenError(tid); - const tsdocs = await getMultiStatus(domainId, { docId: tid }, docType) - .sort(RULES[tdoc.rule].statusSort).toArray(); - const uids = []; - for (const tsdoc of tsdocs) uids.push(tsdoc.uid); + const tsdocs = await getMultiStatus(domainId, { docId: tid }, docType).toArray(); + const uids = tsdocs.map((tsdoc) => tsdoc.uid); const [udict, pdict] = await Promise.all([ user.getList(domainId, uids), problem.getList(domainId, tdoc.pids, true), @@ -535,10 +533,6 @@ export function getMulti(domainId: string, query = {}, type = document.TYPE_CONT return document.getMulti(domainId, type, query); } -function _getStatusJournal(tsdoc) { - return tsdoc.journal.sort((a, b) => (a.rid.generationTime - b.rid.generationTime)); -} - export async function getAndListStatus( domainId: string, tid: ObjectID, docType = document.TYPE_CONTEST, ) { diff --git a/hydro/model/document.ts b/hydro/model/document.ts index 9f2885379..a2c50ef32 100644 --- a/hydro/model/document.ts +++ b/hydro/model/document.ts @@ -307,11 +307,10 @@ export async function revSetStatus( }; const update = { $set: args, $inc: { rev: 1 } }; if (returnDoc) { - const res = await coll.findOneAndUpdate(filter, update, { returnOriginal: false }); + const res = await collStatus.findOneAndUpdate(filter, update, { returnOriginal: false }); return res.value; } - const res = await coll.updateOne(filter, update); - return res; + return await collStatus.updateOne(filter, update); } export async function ensureIndexes() { diff --git a/hydro/model/problem.ts b/hydro/model/problem.ts index 1388ea0ce..9f1027bea 100644 --- a/hydro/model/problem.ts +++ b/hydro/model/problem.ts @@ -3,7 +3,7 @@ import { STATUS } from './builtin'; import * as file from './file'; import * as document from './document'; import * as domain from './domain'; -import { Pdoc } from '../interface'; +import { Pdoc, Pdict } from '../interface'; import { ProblemNotFoundError } from '../error'; import readConfig from '../lib/readConfig'; @@ -103,7 +103,7 @@ export async function random(domainId: string, query: FilterQuery) { export async function getList( domainId: string, pids: Array, getHidden = false, doThrow = true, -) { +): Promise { pids = Array.from(new Set(pids)); const r = {}; const q: any = { $or: [{ docId: { $in: pids } }, { pid: { $in: pids } }] }; diff --git a/hydro/model/record.ts b/hydro/model/record.ts index 1121adae6..45db48d0b 100644 --- a/hydro/model/record.ts +++ b/hydro/model/record.ts @@ -112,8 +112,8 @@ export async function update( return res.value; } -export function reset(domainId: string, rid: ObjectID) { - return update(domainId, rid, { +export function reset(domainId: string, rid: ObjectID, isRejudge: boolean) { + const upd: any = { score: 0, status: STATUS.STATUS_WAITING, time: 0, @@ -123,8 +123,9 @@ export function reset(domainId: string, rid: ObjectID) { compilerTexts: [], judgeAt: null, judger: null, - rejudged: true, - }); + }; + if (isRejudge) upd.rejudged = true; + return update(domainId, rid, upd); } export function count(domainId: string, query: any) { @@ -176,7 +177,7 @@ export async function judge(domainId: string, rid: ObjectID) { } export async function rejudge(domainId: string, rid: ObjectID) { - await reset(domainId, rid); + await reset(domainId, rid, true); const rdoc = await get(domainId, rid); const pdoc = await problem.get(domainId, rdoc.pid); await task.add({ diff --git a/hydro/model/setting.ts b/hydro/model/setting.ts index b94913cde..d9933b3d1 100644 --- a/hydro/model/setting.ts +++ b/hydro/model/setting.ts @@ -117,6 +117,9 @@ SystemSetting( Setting('setting_server', 'server.url', null, null, 'text', 'Server BaseURL'), Setting('setting_server', 'server.port', null, 8888, 'number', 'Server Port'), Setting('setting_server', 'server.log', null, false, 'boolean', 'Disable Access Log'), + Setting('setting_ui', 'ui.header', null, null, 'text', 'Header Logo'), + Setting('setting_ui', 'ui.headerBackground', null, null, 'text', 'Header Background'), + Setting('setting_ui', 'ui.nav', null, null, 'text', 'Nav Logo'), Setting('setting_oauth', 'oauth.githubappid', null, null, 'text', 'Github Oauth AppID'), Setting('setting_oauth', 'oauth.githubsecret', null, null, 'text', 'Github Oauth Secret', null, FLAG_SECRET), Setting('setting_oauth', 'oauth.googleappid', null, null, 'text', 'Google Oauth ClientID', null), diff --git a/hydro/model/user.ts b/hydro/model/user.ts index 550939831..f3dc3f867 100644 --- a/hydro/model/user.ts +++ b/hydro/model/user.ts @@ -6,6 +6,7 @@ import { BUILTIN_ROLES, BUILTIN_USERS, PRIV } from './builtin'; import { UserNotFoundError, UserAlreadyExistError, LoginError } from '../error'; import pwhash from '../lib/hash.hydro'; import * as db from '../service/db'; +import { Udoc, Udict } from '../interface'; const coll = db.collection('user'); @@ -17,7 +18,7 @@ export function setPassword(uid: number, password: string) { ); } -export class User { +export class User implements Udoc { udoc: () => any; dudoc: () => any; @@ -85,7 +86,7 @@ export class User { } hasPriv(p: number) { - return this.priv & p; + return !!(this.priv & p); } checkPassword(password: string) { @@ -109,19 +110,16 @@ export async function getInDomain(domainId: string, udoc: User) { return dudoc; } -export async function getById(domainId: string, _id: number, throwError = false) { +export async function getById(domainId: string, _id: number): Promise { const udoc = _id === 0 || _id === 1 ? BUILTIN_USERS[_id] : await coll.findOne({ _id }); - if (!udoc) { - if (throwError) throw new UserNotFoundError(_id); - else return null; - } + if (!udoc) return null; const dudoc = await getInDomain(domainId, udoc); return new User(udoc, dudoc); } -export async function getList(domainId: string, uids: number[]) { +export async function getList(domainId: string, uids: number[]): Promise { const _uids = new Set(uids); const r = {}; // eslint-disable-next-line no-await-in-loop @@ -129,7 +127,9 @@ export async function getList(domainId: string, uids: number[]) { return r; } -export async function getByUname(domainId: string, uname: string, ignoreMissing = false) { +export async function getByUname( + domainId: string, uname: string, ignoreMissing = false, +): Promise { const unameLower = uname.trim().toLowerCase(); const udoc = (unameLower === 'guest') ? BUILTIN_USERS[0] @@ -144,17 +144,14 @@ export async function getByUname(domainId: string, uname: string, ignoreMissing return new User(udoc, dudoc); } -export async function getByEmail(domainId: string, mail: string, ignoreMissing = false) { +export async function getByEmail(domainId: string, mail: string): Promise { const mailLower = mail.trim().toLowerCase(); const udoc = (mailLower === 'guest@hydro.local') ? BUILTIN_USERS[0] : mailLower === 'hydro@hydro.local' ? BUILTIN_USERS[1] : await coll.findOne({ mailLower }); - if (!udoc) { - if (ignoreMissing) return null; - throw new UserNotFoundError(mail); - } + if (!udoc) return null; const dudoc = await getInDomain(domainId, udoc); return new User(udoc, dudoc); } @@ -172,6 +169,7 @@ export function setEmail(uid: number, mail: string) { export async function changePassword(uid: number, currentPassword: string, newPassword: string) { const udoc = await getById('system', uid); + if (!udoc) throw new UserNotFoundError(uid); udoc.checkPassword(currentPassword); const salt = String.random(); return await coll.findOneAndUpdate( diff --git a/hydro/script/install.ts b/hydro/script/install.ts index 27a4eaa51..70b21f132 100644 --- a/hydro/script/install.ts +++ b/hydro/script/install.ts @@ -44,7 +44,7 @@ export async function run({ username = '', password = '' } = {}) { await Promise.all(tasks); await domain.add('system', 0, 'Hydro', true); if (username && password) { - const udoc = await user.getById('system', -1, false); + const udoc = await user.getById('system', -1); if (!udoc) { await user.create({ uid: -1, diff --git a/hydro/service/server.ts b/hydro/service/server.ts index d2f2d1abb..74cf0bcb8 100644 --- a/hydro/service/server.ts +++ b/hydro/service/server.ts @@ -376,7 +376,7 @@ export const Handler = HandlerMixin(class { this.getBdoc(), ]); [this.user] = await Promise.all([ - user.getById(domainId, this.session.uid, true), + user.getById(domainId, this.session.uid), this.getCsrfToken(), ]); } diff --git a/templates/contest_scoreboard.html b/templates/contest_scoreboard.html index 87f1ba001..d7336ab2b 100644 --- a/templates/contest_scoreboard.html +++ b/templates/contest_scoreboard.html @@ -14,15 +14,15 @@ {%- for column in rows[0] -%} - + {%- endfor -%} {%- for column in rows[0] -%} -
+ {% if column.type == 'problem_detail' %} - {{ column['value'] }} + {{ column.value }} {% else %} {{ column.value }} {% endif %} @@ -36,7 +36,7 @@ {%- for column in row -%} {% if column.type == 'user' %} - {{ user.render_inline(column.raw, badge=false) }} + {{ user.render_inline(udict[column.raw], badge=false) }} {% elif column.type == 'record' and column.raw %} {{ column.value }} {% else %} diff --git a/templates/extra.css b/templates/extra.css new file mode 100644 index 000000000..af040a676 --- /dev/null +++ b/templates/extra.css @@ -0,0 +1,10 @@ +/* {% if header %} */ +.header__logo { + background: url({{ header }}) !important +} +/* {% endif %} */ +/* {% if headerBackground %} */ +.header { + background: url({{ headerBackground }}) !important +} +/* {% endif %} */ diff --git a/templates/layout/html5.html b/templates/layout/html5.html index 21f552599..46f670096 100644 --- a/templates/layout/html5.html +++ b/templates/layout/html5.html @@ -23,6 +23,7 @@ + {{ handler.renderTitle(page_name) }} {% if not isIE(handler.request.headers['user-agent']) %}