From f1cbe155c7990b1b1b99662484dd4607413151df Mon Sep 17 00:00:00 2001 From: undefined Date: Sat, 15 Oct 2022 18:56:58 +0800 Subject: [PATCH] ui: contest_problemlist --- packages/hydrooj/src/handler/contest.ts | 11 ++- packages/hydrooj/src/handler/problem.ts | 6 +- packages/hydrooj/src/handler/record.ts | 30 ++++---- packages/hydrooj/src/interface.ts | 2 +- packages/hydrooj/src/model/contest.ts | 18 ++--- packages/hydrooj/src/model/task.ts | 2 +- packages/ui-default/backendlib/template.js | 1 + .../components/contest/contest.page.styl | 72 +++++++++++++++++++ packages/ui-default/pages/contest.page.ts | 2 +- .../templates/contest_scoreboard.html | 1 + .../partials/problem_sidebar_contest.html | 13 ++++ 11 files changed, 127 insertions(+), 31 deletions(-) diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index f865cd8d4..c85b67dca 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -127,7 +127,12 @@ export class ContestDetailBaseHandler extends Handler { }, { name: 'record_main', - args: { tid, prefix: 'record_main', query: contest.canShowRecord.call(this, this.tdoc, true) ? {} : { uidOrName: this.user._id } }, + args: { + prefix: 'record', + query: contest.canShowRecord.call(this, this.tdoc, true) + ? { tid } + : { tid, uidOrName: this.user._id }, + }, checker: () => contest.canShowSelfRecord.call(this, this.tdoc, true), }, { @@ -163,14 +168,14 @@ export class ContestDetailHandler extends ContestDetailBaseHandler { && !this.user.hasPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD) ) return; const pdict = await problem.getList(domainId, this.tdoc.pids, true, undefined, undefined, problem.PROJECTION_CONTEST_LIST); - const psdict = {}; + let psdict = {}; let rdict = {}; 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() }); this.tsdoc.startAt = new Date(); } - for (const pdetail of this.tsdoc.journal || []) psdict[pdetail.pid] = pdetail; + psdict = this.tsdoc.detail || {}; if (contest.canShowSelfRecord.call(this, this.tdoc)) { rdict = await record.getList(domainId, Object.values(psdict).map((i: any) => i.rid)); } else { diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index 18a0207c7..7e671dd9a 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -377,8 +377,12 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler { solutionCount: scnt, discussionCount: dcnt, tdoc: this.tdoc, - tsdoc: pick(this.tsdoc, ['attend', 'startAt']), }; + if (this.tdoc && this.tsdoc) { + const fields = ['attend', 'startAt']; + if (contest.canShowSelfRecord.call(this, this.tdoc, true)) fields.push('detail'); + this.response.body.tsdoc = pick(this.tsdoc, fields); + } this.response.template = 'problem_detail.html'; this.UiContext.extraTitleContent = this.pdoc.title; } diff --git a/packages/hydrooj/src/handler/record.ts b/packages/hydrooj/src/handler/record.ts index 203d99f14..d1fd47a17 100644 --- a/packages/hydrooj/src/handler/record.ts +++ b/packages/hydrooj/src/handler/record.ts @@ -15,13 +15,14 @@ import * as system from '../model/system'; import TaskModel from '../model/task'; import user from '../model/user'; import * as bus from '../service/bus'; -import { - ConnectionHandler, Handler, param, Types, -} from '../service/server'; +import { ConnectionHandler, param, Types } from '../service/server'; import { buildProjection } from '../utils'; +import { ContestDetailBaseHandler } from './contest'; import { postJudge } from './judge'; -class RecordListHandler extends Handler { +class RecordListHandler extends ContestDetailBaseHandler { + tdoc?: Tdoc<30>; + @param('page', Types.PositiveInt, true) @param('pid', Types.Name, true) @param('tid', Types.ObjectID, true) @@ -42,6 +43,7 @@ class RecordListHandler extends Handler { if (full) uidOrName = this.user._id.toString(); if (tid) { tdoc = await contest.get(domainId, tid); + this.tdoc = tdoc; if (!tdoc) throw new ContestNotFoundError(domainId, pid); if (!contest.canShowScoreboard.call(this, tdoc, true)) throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); if (!this.user.own(tdoc) && !(await contest.getStatus(domainId, tid, this.user._id))?.attend) { @@ -106,8 +108,10 @@ class RecordListHandler extends Handler { } } -class RecordDetailHandler extends Handler { +class RecordDetailHandler extends ContestDetailBaseHandler { rdoc: RecordDoc; + tdoc?: Tdoc<30>; + @param('rid', Types.ObjectID) async prepare(domainId: string, rid: ObjectID) { this.rdoc = await record.get(domainId, rid); @@ -133,15 +137,15 @@ class RecordDetailHandler extends Handler { // eslint-disable-next-line consistent-return async get(domainId: string, rid: ObjectID, download = false) { const rdoc = this.rdoc; - let tdoc: Tdoc<30>; if (rdoc.contest?.toString() === '000000000000000000000000') { if (rdoc.uid !== this.user._id) throw new PermissionError(PERM.PERM_READ_RECORD_CODE); } else if (rdoc.contest) { - tdoc = await contest.get(domainId, rdoc.contest); - let canView = this.user.own(tdoc); - canView ||= contest.canShowRecord.call(this, tdoc); - canView ||= contest.canShowSelfRecord.call(this, tdoc, true) && rdoc.uid === this.user._id; + this.tdoc = await contest.get(domainId, rdoc.contest); + let canView = this.user.own(this.tdoc); + canView ||= contest.canShowRecord.call(this, this.tdoc); + canView ||= contest.canShowSelfRecord.call(this, this.tdoc, true) && rdoc.uid === this.user._id; if (!canView) throw new PermissionError(rid); + this.args.tid = this.tdoc.docId; } // eslint-disable-next-line prefer-const @@ -155,8 +159,8 @@ class RecordDetailHandler extends Handler { canViewCode ||= this.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE); canViewCode ||= this.user.hasPerm(PERM.PERM_READ_RECORD_CODE); canViewCode ||= this.user.hasPerm(PERM.PERM_READ_RECORD_CODE_ACCEPT) && self?.status === STATUS.STATUS_ACCEPTED; - if (tdoc && tdoc.allowViewCode && contest.isDone(tdoc)) { - const tsdoc = await contest.getStatus(domainId, tdoc.docId, this.user._id); + if (this.tdoc && this.tdoc.allowViewCode && contest.isDone(this.tdoc)) { + const tsdoc = await contest.getStatus(domainId, this.tdoc.docId, this.user._id); canViewCode ||= tsdoc?.attend; } if (!canViewCode) { @@ -170,7 +174,7 @@ class RecordDetailHandler extends Handler { this.response.template = 'record_detail.html'; this.response.body = { - udoc, rdoc, pdoc, tdoc, + udoc, rdoc, pdoc, tdoc: this.tdoc, }; } diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 10003ae78..46374ab07 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -483,7 +483,7 @@ export interface OplogDoc extends Record { } export interface ContestStat extends Record { - detail: any, + detail: Record>, } export interface ContestRule { diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index 31885eba1..be353d199 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -63,7 +63,7 @@ const acm = buildContestRule({ stat(tdoc, journal: AcmJournal[], ignoreLock = false) { const naccept = Counter(); const effective: Record = {}; - const detail: AcmDetail[] = []; + const detail: Record = {}; let accept = 0; let time = 0; for (const j of filterEffective(tdoc, journal, ignoreLock)) { @@ -77,11 +77,11 @@ const acm = buildContestRule({ const j = effective[pid]; const real = j.rid.generationTime - Math.floor(tdoc.beginAt.getTime() / 1000); const penalty = 20 * 60 * naccept[j.pid]; - detail.push({ + detail[pid] = { ...j, naccept: naccept[j.pid], time: real + penalty, real, penalty, - }); + }; } - for (const d of detail.filter((i) => i.status === STATUS.STATUS_ACCEPTED)) { + for (const d of Object.values(detail).filter((i) => i.status === STATUS.STATUS_ACCEPTED)) { accept++; time += d.time; } @@ -118,8 +118,7 @@ const acm = buildContestRule({ return columns; }, async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc, meta) { - const tsddict: Record = {}; - for (const item of tsdoc.detail || []) tsddict[item.pid] = item; + const tsddict = tsdoc.detail || {}; const row: ScoreboardRow = [ { type: 'rank', value: rank.toString() }, { type: 'user', value: udoc.uname, raw: tsdoc.uid }, @@ -366,7 +365,7 @@ const homework = buildContestRule({ score: sumBy(detail, 'score'), penaltyScore: sumBy(detail, 'penaltyScore'), time: Math.sum(detail.map((d) => d.time)), - detail, + detail: effective, }; }, showScoreboard: () => true, @@ -410,10 +409,7 @@ const homework = buildContestRule({ return columns; }, async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc) { - const tsddict = {}; - for (const item of tsdoc.detail || []) { - if (!tsddict[item.pid] || tsddict[item.pid].score <= item.score) tsddict[item.pid] = item; - } + const tsddict = tsdoc.detail; const row: ScoreboardRow = [ { type: 'rank', value: rank.toString() }, { diff --git a/packages/hydrooj/src/model/task.ts b/packages/hydrooj/src/model/task.ts index 3abc8cee0..88db45e97 100644 --- a/packages/hydrooj/src/model/task.ts +++ b/packages/hydrooj/src/model/task.ts @@ -76,7 +76,7 @@ class WorkerService extends Service { sleep(300000), ]); const spent = Date.now() - start; - if (spent > 500) logger.warn('Slow worker task (%d ms): %s', spent, doc); + if (spent > 500) logger.warn('Slow worker task (%d ms): %o', spent, doc); } catch (e) { logger.error('Worker task fail: ', e); logger.error('%o', doc); diff --git a/packages/ui-default/backendlib/template.js b/packages/ui-default/backendlib/template.js index 98183eff1..fed28e88d 100644 --- a/packages/ui-default/backendlib/template.js +++ b/packages/ui-default/backendlib/template.js @@ -142,6 +142,7 @@ env.addGlobal('static_url', (assetName) => { env.addGlobal('eval', eval); env.addGlobal('Date', Date); env.addGlobal('Object', Object); +env.addGlobal('String', String); env.addGlobal('Math', Math); env.addGlobal('process', process); env.addGlobal('global', global); diff --git a/packages/ui-default/components/contest/contest.page.styl b/packages/ui-default/components/contest/contest.page.styl index edea712a5..fd15ba7c8 100644 --- a/packages/ui-default/components/contest/contest.page.styl +++ b/packages/ui-default/components/contest/contest.page.styl @@ -40,3 +40,75 @@ for key, value in $contest-color &:hover background: $primary-color border-color: $primary-color + +.contest-problems + font-size: 0.9375rem; + font-weight: 400; + line-height: 1.375rem; + text-align: left; + box-sizing: border-box; + font-variant-ligatures: none!important; + display: grid; + grid-template-columns: repeat(auto-fit,minmax(1.5rem,1fr)); + justify-content: center; + row-gap: 0.75rem; + column-gap: 0.5rem; + padding-bottom: 0.5rem; + margin-top: 1rem; + margin-bottom: 0.5rem; + + > a + text-decoration: none; + background-color: transparent; + cursor: pointer; + display: flex; + height: 1.5rem; + width: 1.5rem; + align-items: center; + justify-content: space-around; + border-radius: 0.375rem; + position: relative; + transition: opacity 0.2s; + color: grey; + &.pass + color: green; + &:before + background: green; + &.fail + color: red; + &:before + background: red; + + &:after + content: ''; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + box-sizing: border-box; + border: 2px solid currentColor; + border-radius: 6px; + opacity: 0.3; + + &.active:after + border: 3px solid cyan; + + &:before + opacity: 0.5; + + &.pass, &.fail + &:not(:hover) .id + display: none; + + &:hover .icon + display: none; + + // .placeholder + // cursor: pointer; + // position: absolute; + // top: -0.375rem; + // right: -0.375rem; + // bottom: -0.375rem; + // left: -0.375rem; + diff --git a/packages/ui-default/pages/contest.page.ts b/packages/ui-default/pages/contest.page.ts index d7eb47d75..956f9572e 100644 --- a/packages/ui-default/pages/contest.page.ts +++ b/packages/ui-default/pages/contest.page.ts @@ -1,7 +1,7 @@ import NProgress from 'nprogress'; import { NamedPage } from 'vj/misc/Page'; -export default new NamedPage(['contest_detail', 'contest_detail_problem'], () => { +export default new NamedPage(['contest_detail', 'contest_detail_problem', 'contest_scoreboard'], () => { const beginAt = new Date(UiContext.tdoc.beginAt).getTime(); const endAt = new Date(UiContext.tdoc.endAt).getTime(); NProgress.configure({ trickle: false, showSpinner: false, minimum: 0 }); diff --git a/packages/ui-default/templates/contest_scoreboard.html b/packages/ui-default/templates/contest_scoreboard.html index c24ba3416..ea77baf1b 100644 --- a/packages/ui-default/templates/contest_scoreboard.html +++ b/packages/ui-default/templates/contest_scoreboard.html @@ -1,5 +1,6 @@ {% import "components/paginator.html" as paginator with context %} {% extends "layout/basic.html" %} +{{ set(UiContext, 'tdoc', tdoc) }} {% set type = 'homework' if tdoc.rule == 'homework' else 'contest' %} {% block content %}
diff --git a/packages/ui-default/templates/partials/problem_sidebar_contest.html b/packages/ui-default/templates/partials/problem_sidebar_contest.html index ede7bc762..318503f10 100644 --- a/packages/ui-default/templates/partials/problem_sidebar_contest.html +++ b/packages/ui-default/templates/partials/problem_sidebar_contest.html @@ -6,6 +6,19 @@
{% endif %}
+
+ {% for pid in tdoc.pids -%} + {% set status = tsdoc.detail[pid].status %} + {% set pass = status == STATUS.STATUS_ACCEPTED %} + {% set fail = status and not pass %} + + {{ String.fromCharCode(65+loop.index0) }} + {% if status %}{% endif %} + + {%- endfor %} +