From 17b38cbf4ea964c9e1836231f8fd85b51c3a0d72 Mon Sep 17 00:00:00 2001 From: undefined Date: Fri, 17 Jul 2020 16:59:39 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=86=E9=A2=98=E7=9B=AE=E7=BA=B3=E5=85=A5ra?= =?UTF-8?q?ting=E8=AE=A1=E7=AE=97=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hydro/entry/common.ts | 4 +- hydro/handler/home.ts | 3 +- hydro/handler/record.ts | 6 +- hydro/interface.ts | 1 + hydro/model/document.ts | 2 +- hydro/model/problem.ts | 1 + hydro/model/task.ts | 37 +++- hydro/model/user.ts | 12 -- hydro/script/rating.ts | 152 +++++++++++++++-- hydro/script/recalcRating.ts | 75 -------- ui/.eslintrc.js | 1 + ui/build/config/webpack.js | 10 -- ui/components/scratchpad/Editor.page.styl | 12 +- ui/components/scratchpad/PanelTab.page.styl | 63 ------- .../scratchpad/ScratchpadEditorContainer.js | 1 + ui/components/scratchpad/index.js | 1 + ui/package.json | 17 +- ui/utils/loadReactRedux.js | 2 +- ui/yarn.lock | 161 ++++++++++-------- 19 files changed, 290 insertions(+), 271 deletions(-) delete mode 100644 hydro/script/recalcRating.ts delete mode 100644 ui/components/scratchpad/PanelTab.page.styl diff --git a/hydro/entry/common.ts b/hydro/entry/common.ts index b5f728627..0fc3b3eeb 100644 --- a/hydro/entry/common.ts +++ b/hydro/entry/common.ts @@ -25,8 +25,8 @@ export const builtinHandler = [ ]; export const builtinScript = [ - 'install', 'rating', 'recalcRating', 'register', 'problemStat', - 'blacklist', 'setSuperadmin', 'deleteUser', + 'install', 'rating', 'register', 'problemStat', 'blacklist', + 'setSuperadmin', 'deleteUser', ]; function getFiles(folder: string, base = ''): string[] { diff --git a/hydro/handler/home.ts b/hydro/handler/home.ts index edb030b71..042838945 100644 --- a/hydro/handler/home.ts +++ b/hydro/handler/home.ts @@ -142,7 +142,8 @@ class HomeSecurityHandler extends Handler { @param('verifyPassword', Types.String) async postChangePassword(current: string, password: string, verifyPassword: string) { if (password !== verifyPassword) throw new VerifyPasswordError(); - await user.changePassword(this.user._id, current, password); + this.user.checkPassword(current); + await user.setPassword(this.user._id, password); this.back(); } diff --git a/hydro/handler/record.ts b/hydro/handler/record.ts index 0831b2425..2e50f51f0 100644 --- a/hydro/handler/record.ts +++ b/hydro/handler/record.ts @@ -50,9 +50,9 @@ class RecordListHandler extends RecordHandler { rdocs, pdict, udict, - fliterPid: pid, - fliterTid: tid, - fliterUidOrName: uidOrName, + filterPid: pid, + filterTid: tid, + filterUidOrName: uidOrName, }; } } diff --git a/hydro/interface.ts b/hydro/interface.ts index 30998bc64..b64c0491b 100644 --- a/hydro/interface.ts +++ b/hydro/interface.ts @@ -56,6 +56,7 @@ export type Udict = NumericDictionary; export interface Pdoc { _id: ObjectID, domainId: string, + docType: number, docId: number, pid: string, owner: number, diff --git a/hydro/model/document.ts b/hydro/model/document.ts index aaa49da21..171439213 100644 --- a/hydro/model/document.ts +++ b/hydro/model/document.ts @@ -238,7 +238,7 @@ export async function setIfNotStatus( { domainId, docType, docId, uid, key: { $not: { $eq: ifNot } }, }, - { $set: { [key]: value, args } }, + { $set: { [key]: value, ...args } }, { upsert: true, returnOriginal: false }, ); return res.value; diff --git a/hydro/model/problem.ts b/hydro/model/problem.ts index 28423d93d..2569e382d 100644 --- a/hydro/model/problem.ts +++ b/hydro/model/problem.ts @@ -10,6 +10,7 @@ import readConfig from '../lib/readConfig'; export const pdocHidden: Pdoc = { _id: new ObjectID(), domainId: 'system', + docType: document.TYPE_PROBLEM, docId: -1, pid: '', owner: 1, diff --git a/hydro/model/task.ts b/hydro/model/task.ts index fa237f5ec..a10da5769 100644 --- a/hydro/model/task.ts +++ b/hydro/model/task.ts @@ -57,6 +57,41 @@ export async function consume(query: any, cb: Function) { }, 100); } +export class Consumer { + consuming: boolean; + + running?: Promise; + + interval: NodeJS.Timeout; + + filter: any; + + func: Function; + + constructor(filter: any, func: Function) { + this.consuming = true; + this.filter = filter; + this.func = func; + this.interval = setInterval(this.consume.bind(this), 100); + } + + async consume() { + if (this.running || !this.consuming) return; + const res = await getFirst(this.filter); + if (res) { + this.running = this.func(res); + if (this.running instanceof Promise) await this.running; + this.running = null; + } + } + + async destory() { + this.consuming = false; + clearInterval(this.interval); + if (this.running) await this.running; + } +} + global.Hydro.model.task = { - add, get, del, count, getFirst, consume, + Consumer, add, get, del, count, getFirst, consume, }; diff --git a/hydro/model/user.ts b/hydro/model/user.ts index ebab3e1a1..da1df668a 100644 --- a/hydro/model/user.ts +++ b/hydro/model/user.ts @@ -156,17 +156,6 @@ export function setEmail(uid: number, mail: string) { return setById(uid, { mail, mailLower: mail.trim().toLowerCase() }); } -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( - { _id: udoc._id }, - { $set: { salt, hash: pwhash(newPassword, salt), hashType: 'hydro' } }, - ); -} - export async function inc(_id: number, field: string, n = 1) { const udoc = await coll.findOne({ _id }); if (!udoc) throw new UserNotFoundError(_id); @@ -234,7 +223,6 @@ export function ensureIndexes() { global.Hydro.model.user = { User, - changePassword, create, getByEmail, getById, diff --git a/hydro/script/rating.ts b/hydro/script/rating.ts index 2e0bb0f7d..1278d5360 100644 --- a/hydro/script/rating.ts +++ b/hydro/script/rating.ts @@ -1,28 +1,150 @@ +/* eslint-disable no-await-in-loop */ +import { NumericDictionary } from 'lodash'; import { ObjectID } from 'mongodb'; +import * as domain from '../model/domain'; import * as contest from '../model/contest'; -import * as user from '../model/user'; +import * as problem from '../model/problem'; import rating from '../lib/rating'; +import { STATUS } from '../model/builtin'; +import { Tdoc, Pdoc } from '../interface'; -export const description = 'Calculate the rating of a contest'; +export const description = 'Calculate rating of a domain, or all domains'; -export async function run({ domainId, contestId }) { - contestId = new ObjectID(contestId); - const tdoc = await contest.get(domainId, contestId); - const tsdocs = await contest.getMultiStatus(domainId, { docId: contestId }) - .sort(contest.RULES[tdoc.rule].statusSort).toArray(); - const udict = await user.getList(domainId, tsdocs.map((tsdoc) => tsdoc.uid)); - const rankedTsdocs = contest.RULES[tdoc.rule].rank(tsdocs); +type ND = NumericDictionary + +function calc(udict: ND, rankedDocs: [number, number][]) { const users = []; - for (const result of rankedTsdocs) { - users.push({ ...result[1], rank: result[0], old: udict[result[1].uid].rating || 1500 }); + for (const [rank, uid] of rankedDocs) { + users.push({ uid, rank, old: users[uid] || 1500 }); } const rated = rating(users); - return rated; + for (const udoc of rated) { + udict[udoc.uid] = udoc.new; + } +} + +async function runProblem(pdoc: Pdoc, udict: ND, report: Function): Promise +async function runProblem( + domainId: string, pid: number, udict: ND, report: Function +): Promise +async function runProblem(...arg: Array) { + const start = new Date().getTime(); + const pdoc: Pdoc = (typeof arg[0] === 'string') + ? await contest.get(arg[0], arg[1], -1) + : arg[0]; + const udict: ND = (typeof arg[0] === 'string') ? arg[2] : arg[1]; + const report = (typeof arg[0] === 'string') ? arg[3] : arg[2]; + // TODO maybe some other rules? + // TODO pagination + const psdocs = await problem.getMultiStatus( + pdoc.domainId, { docId: pdoc.docId, status: STATUS.STATUS_ACCEPTED }, + ).sort('rid', 1).toArray(); + const ranked = []; + for (const index of psdocs) { + ranked.push([index + 1, psdocs[index].uid]); + } + calc(udict, ranked); + await report({ + case: { + status: STATUS.STATUS_ACCEPTED, + judgeText: `Problem ${pdoc.title} finished`, + time_ms: new Date().getTime() - start, + memory_kb: 0, + score: 0, + }, + }); +} + +async function runContest(tdoc: Tdoc, udict: ND, report: Function): Promise +async function runContest( + domainId: string, tid: ObjectID, udict: ND, report: Function +): Promise +async function runContest(...arg: Array) { + const start = new Date().getTime(); + const tdoc: Tdoc = (typeof arg[0] === 'string') + ? await contest.get(arg[0], arg[1], -1) + : arg[0]; + const udict: ND = (typeof arg[0] === 'string') ? arg[2] : arg[1]; + const report = (typeof arg[0] === 'string') ? arg[3] : arg[2]; + const tsdocs = await contest.getMultiStatus(tdoc.domainId, tdoc.docId, tdoc.docType) + .sort(contest.RULES[tdoc.rule].statusSort).toArray(); + const rankedTsdocs = contest.RULES[tdoc.rule].rank(tsdocs); + const ranked = []; + for (const result of rankedTsdocs) { + ranked.push([result[0], result[1].uid]); + } + calc(udict, ranked); + await report({ + case: { + status: STATUS.STATUS_ACCEPTED, + judgeText: `Contest ${tdoc.title} finished`, + time_ms: new Date().getTime() - start, + memory_kb: 0, + score: 0, + }, + }); +} + +async function runInDomain(domainId: string, isSub: boolean, report: Function) { + await domain.setMultiUserInDomain(domainId, {}, { rating: 1500 }); + const udict: ND = {}; + // TODO pagination + const problems = await problem.getMulti(domainId, { hidden: false }).toArray(); + await report({ message: `Found ${problems.length} problems in ${domainId}` }); + for (const i in problems) { + const pdoc = problems[i]; + runProblem(pdoc, udict, report); + if (!isSub) { + await report({ + progress: Math.floor(((parseInt(i, 10) + 1) / problems.length) * 100), + }); + } + } + const contests = await contest.getMulti(domainId, { rated: true }, -1).sort('endAt', -1).toArray(); + await report({ message: `Found ${contests.length} contests in ${domainId}` }); + for (const i in contests) { + const tdoc = contests[i]; + runContest(tdoc, udict, report); + if (!isSub) { + await report({ + progress: Math.floor(((parseInt(i, 10) + 1) / contests.length) * 100), + }); + } + } + const tasks = []; + for (const uid in udict) { + tasks.push(domain.setUserInDomain(domainId, parseInt(uid, 10), { rating: udict[uid] })); + } + await Promise.all(tasks); +} + +export async function run({ domainId }, report: Function) { + if (!domainId) { + const domains = await domain.getMulti().toArray(); + await report({ message: `Found ${domains.length} domains` }); + for (const i in domains) { + const start = new Date().getTime(); + await runInDomain(domains[i]._id, true, report); + await report({ + case: { + status: STATUS.STATUS_ACCEPTED, + judgeText: `Domain ${domains[i]._id} finished`, + time_ms: new Date().getTime() - start, + memory_kb: 0, + score: 0, + }, + progress: Math.floor(((parseInt(i, 10) + 1) / domains.length) * 100), + }); + } + } else runInDomain(domainId, false, report); + return true; } export const validate = { - domainId: { $type: 'string' }, - contestId: { $type: ObjectID }, + $or: [ + { domainId: 'string' }, + { domainId: 'undefined' }, + ], }; -global.Hydro.script.rating = { run, description, validate }; +global.Hydro.script.recalcRating = { run, description, validate }; diff --git a/hydro/script/recalcRating.ts b/hydro/script/recalcRating.ts deleted file mode 100644 index e4a03c31d..000000000 --- a/hydro/script/recalcRating.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* eslint-disable no-await-in-loop */ -import * as domain from '../model/domain'; -import * as contest from '../model/contest'; -import rating from '../lib/rating'; -import { STATUS } from '../model/builtin'; - -export const description = 'Calculate rating of a domain, or all domains'; - -export async function run({ domainId, isSub = false }, report: Function) { - if (!domainId) { - const domains = await domain.getMulti().toArray(); - await report({ message: `Found ${domains.length} domains` }); - for (const i in domains) { - const start = new Date().getTime(); - await run({ domainId: domains[i]._id, isSub: true }, report); - await report({ - case: { - status: STATUS.STATUS_ACCEPTED, - judgeText: `Domain ${domains[i]._id} finished`, - time_ms: new Date().getTime() - start, - memory_kb: 0, - score: 0, - }, - progress: Math.floor(((parseInt(i, 10) + 1) / domains.length) * 100), - }); - } - return true; - } - await domain.setMultiUserInDomain(domainId, {}, { rating: 1500 }); - const contests = await contest.getMulti(domainId, { rated: true }).sort('endAt', -1).toArray(); - await report({ message: `Found ${contests.length} contests in ${domainId}` }); - const udict = {}; - for (const i in contests) { - const start = new Date().getTime(); - const tdoc = contests[i]; - const tsdocs = await contest.getMultiStatus(domainId, { docId: tdoc.docId }) - .sort(contest.RULES[tdoc.rule].statusSort).toArray(); - const rankedTsdocs = contest.RULES[tdoc.rule].rank(tsdocs); - const users = []; - for (const result of rankedTsdocs) { - users.push({ ...result[1], rank: result[0], old: udict[result[1].uid] || 1500 }); - } - const rated = rating(users); - for (const udoc of rated) { - udict[udoc.uid] = udoc.new; - } - if (!isSub) { - await report({ progress: Math.floor(((parseInt(i, 10) + 1) / contests.length) * 100) }); - } - await report({ - case: { - status: STATUS.STATUS_ACCEPTED, - judgeText: `Contest ${tdoc.title} finished`, - time_ms: new Date().getTime() - start, - memory_kb: 0, - score: 0, - }, - }); - } - const tasks = []; - for (const uid in udict) { - tasks.push(domain.setUserInDomain(domainId, parseInt(uid, 10), { rating: udict[uid] })); - } - await Promise.all(tasks); - return true; -} - -export const validate = { - $or: [ - { domainId: 'string' }, - { domainId: 'undefined' }, - ], -}; - -global.Hydro.script.recalcRating = { run, description, validate }; diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js index 4ae338472..6e198ed41 100644 --- a/ui/.eslintrc.js +++ b/ui/.eslintrc.js @@ -1,3 +1,4 @@ +// eslint-disable-next-line import/no-extraneous-dependencies const path = require('path'); module.exports = { diff --git a/ui/build/config/webpack.js b/ui/build/config/webpack.js index a40d1131d..b900c7f25 100644 --- a/ui/build/config/webpack.js +++ b/ui/build/config/webpack.js @@ -120,16 +120,6 @@ export default function (env = {}) { exclude: /node_modules[/\\]/, use: [babelLoader()], }, - { - // fix pickadate loading - test: /pickadate/, - use: [ - { - loader: 'imports-loader', - options: { define: '>false' }, - }, - ], - }, { test: /\.styl$/, use: [extractCssLoader(), cssLoader(), postcssLoader(), 'stylus-loader'] diff --git a/ui/components/scratchpad/Editor.page.styl b/ui/components/scratchpad/Editor.page.styl index 9eb3fcbf7..4728d4df4 100644 --- a/ui/components/scratchpad/Editor.page.styl +++ b/ui/components/scratchpad/Editor.page.styl @@ -1,4 +1,4 @@ -.ReactCodeMirror +.ScratchpadMonacoEditor flex: 1 position: relative overflow: hidden @@ -12,13 +12,3 @@ font-family: $code-font-family font-size: rem($font-size-secondary) line-height: 1.2 - - &.CodeMirror-focused .CodeMirror-selected - background: #cee4f2 - - .CodeMirror-gutters - border-right: 0 - background: none - - .CodeMirror-linenumber - color: #DDD diff --git a/ui/components/scratchpad/PanelTab.page.styl b/ui/components/scratchpad/PanelTab.page.styl deleted file mode 100644 index 855c4f799..000000000 --- a/ui/components/scratchpad/PanelTab.page.styl +++ /dev/null @@ -1,63 +0,0 @@ -@import './var.inc.styl' - -.scratchpad__panel-tab - margin-top: rem(-($panel-header-height)) - - &.rc-tabs-top - border: 0 - - &.rc-tabs-top .rc-tabs-bar - border: 0 - - .rc-tabs-bar - padding-left: rem(120px) - - .rc-tabs-ink-bar - background: $tab-color - - .rc-tabs-tab-prev-icon, .rc-tabs-tab-next-icon - display: none - font-family: $icon-font-name !important - line-height: rem($panel-header-height) - font-size: rem($font-size) - color: #FFF - - .rc-tabs-tab-arrow-show - display: block - - &.rc-tabs-top .rc-tabs-tab-prev-icon:before - content: $icon-chevron_left - - &.rc-tabs-top .rc-tabs-tab-next-icon:before - content: $icon-chevron_right - - .rc-tabs-content - flex: 1 - - .rc-tabs-tabpane - display: flex - flex-direction: column - - &.rc-tabs-top .rc-tabs-tab - padding: rem(0 5px) - line-height: rem($panel-header-height) - margin: 0 - margin-right: rem(5px) - color: rgba(#FFF, 0.8) - - &.rc-tabs-top .rc-tabs-tab-active - &, &:hover - color: #FFF - - .rc-tabs-bar, .rc-tabs-nav-container - line-height: 1 - font-size: rem($font-size-small) - - .rc-tabs-tab-btn-disabled - color: rgba(#FFF, 0.7) - - .rc-tabs-tabpane-hidden - display: none - - .rc-tabs-tabpane - overflow: auto diff --git a/ui/components/scratchpad/ScratchpadEditorContainer.js b/ui/components/scratchpad/ScratchpadEditorContainer.js index dabd0f1eb..d01752834 100644 --- a/ui/components/scratchpad/ScratchpadEditorContainer.js +++ b/ui/components/scratchpad/ScratchpadEditorContainer.js @@ -30,6 +30,7 @@ export default class ScratchpadEditorContainer extends React.PureComponent {