ui: contest_problemlist

This commit is contained in:
undefined committed 2022-10-15 18:56:58 +08:00
1 parent add93f5783
commit f1cbe155c7
11 files changed
+127 -31

No files matched your search

+8 -3
View File
@@ -127,7 +127,12 @@ export class ContestDetailBaseHandler extends Handler {
}, },
{ {
name: 'record_main', 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), 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) && !this.user.hasPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD)
) return; ) return;
const pdict = await problem.getList(domainId, this.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 psdict = {};
let rdict = {}; let rdict = {};
if (this.tsdoc) { if (this.tsdoc) {
if (this.tsdoc.attend && !this.tsdoc.startAt && contest.isOngoing(this.tdoc)) { if (this.tsdoc.attend && !this.tsdoc.startAt && contest.isOngoing(this.tdoc)) {
await contest.setStatus(domainId, tid, this.user._id, { startAt: new Date() }); await contest.setStatus(domainId, tid, this.user._id, { startAt: new Date() });
this.tsdoc.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)) { if (contest.canShowSelfRecord.call(this, this.tdoc)) {
rdict = await record.getList(domainId, Object.values(psdict).map((i: any) => i.rid)); rdict = await record.getList(domainId, Object.values(psdict).map((i: any) => i.rid));
} else { } else {
+5 -1
View File
@@ -377,8 +377,12 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler {
solutionCount: scnt, solutionCount: scnt,
discussionCount: dcnt, discussionCount: dcnt,
tdoc: this.tdoc, 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.response.template = 'problem_detail.html';
this.UiContext.extraTitleContent = this.pdoc.title; this.UiContext.extraTitleContent = this.pdoc.title;
} }
+17 -13
View File
@@ -15,13 +15,14 @@ import * as system from '../model/system';
import TaskModel from '../model/task'; import TaskModel from '../model/task';
import user from '../model/user'; import user from '../model/user';
import * as bus from '../service/bus'; import * as bus from '../service/bus';
import { import { ConnectionHandler, param, Types } from '../service/server';
ConnectionHandler, Handler, param, Types,
} from '../service/server';
import { buildProjection } from '../utils'; import { buildProjection } from '../utils';
import { ContestDetailBaseHandler } from './contest';
import { postJudge } from './judge'; import { postJudge } from './judge';
class RecordListHandler extends Handler { class RecordListHandler extends ContestDetailBaseHandler {
tdoc?: Tdoc<30>;
@param('page', Types.PositiveInt, true) @param('page', Types.PositiveInt, true)
@param('pid', Types.Name, true) @param('pid', Types.Name, true)
@param('tid', Types.ObjectID, true) @param('tid', Types.ObjectID, true)
@@ -42,6 +43,7 @@ class RecordListHandler extends Handler {
if (full) uidOrName = this.user._id.toString(); if (full) uidOrName = this.user._id.toString();
if (tid) { if (tid) {
tdoc = await contest.get(domainId, tid); tdoc = await contest.get(domainId, tid);
this.tdoc = tdoc;
if (!tdoc) throw new ContestNotFoundError(domainId, pid); if (!tdoc) throw new ContestNotFoundError(domainId, pid);
if (!contest.canShowScoreboard.call(this, tdoc, true)) throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); 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) { 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; rdoc: RecordDoc;
tdoc?: Tdoc<30>;
@param('rid', Types.ObjectID) @param('rid', Types.ObjectID)
async prepare(domainId: string, rid: ObjectID) { async prepare(domainId: string, rid: ObjectID) {
this.rdoc = await record.get(domainId, rid); this.rdoc = await record.get(domainId, rid);
@@ -133,15 +137,15 @@ class RecordDetailHandler extends Handler {
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
async get(domainId: string, rid: ObjectID, download = false) { async get(domainId: string, rid: ObjectID, download = false) {
const rdoc = this.rdoc; const rdoc = this.rdoc;
let tdoc: Tdoc<30>;
if (rdoc.contest?.toString() === '000000000000000000000000') { if (rdoc.contest?.toString() === '000000000000000000000000') {
if (rdoc.uid !== this.user._id) throw new PermissionError(PERM.PERM_READ_RECORD_CODE); if (rdoc.uid !== this.user._id) throw new PermissionError(PERM.PERM_READ_RECORD_CODE);
} else if (rdoc.contest) { } else if (rdoc.contest) {
tdoc = await contest.get(domainId, rdoc.contest); this.tdoc = await contest.get(domainId, rdoc.contest);
let canView = this.user.own(tdoc); let canView = this.user.own(this.tdoc);
canView ||= contest.canShowRecord.call(this, tdoc); canView ||= contest.canShowRecord.call(this, this.tdoc);
canView ||= contest.canShowSelfRecord.call(this, tdoc, true) && rdoc.uid === this.user._id; canView ||= contest.canShowSelfRecord.call(this, this.tdoc, true) && rdoc.uid === this.user._id;
if (!canView) throw new PermissionError(rid); if (!canView) throw new PermissionError(rid);
this.args.tid = this.tdoc.docId;
} }
// eslint-disable-next-line prefer-const // 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.hasPriv(PRIV.PRIV_READ_RECORD_CODE);
canViewCode ||= this.user.hasPerm(PERM.PERM_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; canViewCode ||= this.user.hasPerm(PERM.PERM_READ_RECORD_CODE_ACCEPT) && self?.status === STATUS.STATUS_ACCEPTED;
if (tdoc && tdoc.allowViewCode && contest.isDone(tdoc)) { if (this.tdoc && this.tdoc.allowViewCode && contest.isDone(this.tdoc)) {
const tsdoc = await contest.getStatus(domainId, tdoc.docId, this.user._id); const tsdoc = await contest.getStatus(domainId, this.tdoc.docId, this.user._id);
canViewCode ||= tsdoc?.attend; canViewCode ||= tsdoc?.attend;
} }
if (!canViewCode) { if (!canViewCode) {
@@ -170,7 +174,7 @@ class RecordDetailHandler extends Handler {
this.response.template = 'record_detail.html'; this.response.template = 'record_detail.html';
this.response.body = { this.response.body = {
udoc, rdoc, pdoc, tdoc, udoc, rdoc, pdoc, tdoc: this.tdoc,
}; };
} }
+1 -1
View File
@@ -483,7 +483,7 @@ export interface OplogDoc extends Record<string, any> {
} }
export interface ContestStat extends Record<string, any> { export interface ContestStat extends Record<string, any> {
detail: any, detail: Record<number, Record<string, any>>,
} }
export interface ContestRule<T = any> { export interface ContestRule<T = any> {
+7 -11
View File
@@ -63,7 +63,7 @@ const acm = buildContestRule({
stat(tdoc, journal: AcmJournal[], ignoreLock = false) { stat(tdoc, journal: AcmJournal[], ignoreLock = false) {
const naccept = Counter<number>(); const naccept = Counter<number>();
const effective: Record<number, AcmJournal> = {}; const effective: Record<number, AcmJournal> = {};
const detail: AcmDetail[] = []; const detail: Record<number, AcmDetail> = {};
let accept = 0; let accept = 0;
let time = 0; let time = 0;
for (const j of filterEffective(tdoc, journal, ignoreLock)) { for (const j of filterEffective(tdoc, journal, ignoreLock)) {
@@ -77,11 +77,11 @@ const acm = buildContestRule({
const j = effective[pid]; const j = effective[pid];
const real = j.rid.generationTime - Math.floor(tdoc.beginAt.getTime() / 1000); const real = j.rid.generationTime - Math.floor(tdoc.beginAt.getTime() / 1000);
const penalty = 20 * 60 * naccept[j.pid]; const penalty = 20 * 60 * naccept[j.pid];
detail.push({ detail[pid] = {
...j, naccept: naccept[j.pid], time: real + penalty, real, penalty, ...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++; accept++;
time += d.time; time += d.time;
} }
@@ -118,8 +118,7 @@ const acm = buildContestRule({
return columns; return columns;
}, },
async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc, meta) { async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc, meta) {
const tsddict: Record<number, AcmDetail> = {}; const tsddict = tsdoc.detail || {};
for (const item of tsdoc.detail || []) tsddict[item.pid] = item;
const row: ScoreboardRow = [ const row: ScoreboardRow = [
{ type: 'rank', value: rank.toString() }, { type: 'rank', value: rank.toString() },
{ type: 'user', value: udoc.uname, raw: tsdoc.uid }, { type: 'user', value: udoc.uname, raw: tsdoc.uid },
@@ -366,7 +365,7 @@ const homework = buildContestRule({
score: sumBy(detail, 'score'), score: sumBy(detail, 'score'),
penaltyScore: sumBy(detail, 'penaltyScore'), penaltyScore: sumBy(detail, 'penaltyScore'),
time: Math.sum(detail.map((d) => d.time)), time: Math.sum(detail.map((d) => d.time)),
detail, detail: effective,
}; };
}, },
showScoreboard: () => true, showScoreboard: () => true,
@@ -410,10 +409,7 @@ const homework = buildContestRule({
return columns; return columns;
}, },
async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc) { async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc) {
const tsddict = {}; const tsddict = tsdoc.detail;
for (const item of tsdoc.detail || []) {
if (!tsddict[item.pid] || tsddict[item.pid].score <= item.score) tsddict[item.pid] = item;
}
const row: ScoreboardRow = [ const row: ScoreboardRow = [
{ type: 'rank', value: rank.toString() }, { type: 'rank', value: rank.toString() },
{ {
+1 -1
View File
@@ -76,7 +76,7 @@ class WorkerService extends Service {
sleep(300000), sleep(300000),
]); ]);
const spent = Date.now() - start; 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) { } catch (e) {
logger.error('Worker task fail: ', e); logger.error('Worker task fail: ', e);
logger.error('%o', doc); logger.error('%o', doc);
@@ -142,6 +142,7 @@ env.addGlobal('static_url', (assetName) => {
env.addGlobal('eval', eval); env.addGlobal('eval', eval);
env.addGlobal('Date', Date); env.addGlobal('Date', Date);
env.addGlobal('Object', Object); env.addGlobal('Object', Object);
env.addGlobal('String', String);
env.addGlobal('Math', Math); env.addGlobal('Math', Math);
env.addGlobal('process', process); env.addGlobal('process', process);
env.addGlobal('global', global); env.addGlobal('global', global);
@@ -40,3 +40,75 @@ for key, value in $contest-color
&:hover &:hover
background: $primary-color background: $primary-color
border-color: $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;
+1 -1
View File
@@ -1,7 +1,7 @@
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import { NamedPage } from 'vj/misc/Page'; 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 beginAt = new Date(UiContext.tdoc.beginAt).getTime();
const endAt = new Date(UiContext.tdoc.endAt).getTime(); const endAt = new Date(UiContext.tdoc.endAt).getTime();
NProgress.configure({ trickle: false, showSpinner: false, minimum: 0 }); NProgress.configure({ trickle: false, showSpinner: false, minimum: 0 });
@@ -1,5 +1,6 @@
{% import "components/paginator.html" as paginator with context %} {% import "components/paginator.html" as paginator with context %}
{% extends "layout/basic.html" %} {% extends "layout/basic.html" %}
{{ set(UiContext, 'tdoc', tdoc) }}
{% set type = 'homework' if tdoc.rule == 'homework' else 'contest' %} {% set type = 'homework' if tdoc.rule == 'homework' else 'contest' %}
{% block content %} {% block content %}
<div class="row"><div class="medium-12 columns scoreboard--{{ tdoc.rule }}"> <div class="row"><div class="medium-12 columns scoreboard--{{ tdoc.rule }}">
@@ -6,6 +6,19 @@
</div> </div>
{% endif %} {% endif %}
<div> <div>
<div class="contest-problems">
{% for pid in tdoc.pids -%}
{% set status = tsdoc.detail[pid].status %}
{% set pass = status == STATUS.STATUS_ACCEPTED %}
{% set fail = status and not pass %}
<a href="{{ url('problem_detail', pid=pid, query={tid:tdoc.docId}) }}"
class="{% if pass %}pass{% elif fail %}fail{% endif %}
{% if pid==pdoc.docId %} active{% endif %}">
<span class="id">{{ String.fromCharCode(65+loop.index0) }}</span>
{% if status %}<span class="icon icon-{% if pass %}check{% else %}close{% endif %}"></span>{% endif %}
</a>
{%- endfor %}
</div>
<ol class="menu"> <ol class="menu">
{% if model.contest.isOngoing(tdoc, tsdoc) %} {% if model.contest.isOngoing(tdoc, tsdoc) %}
{% if page_name == 'contest_detail_problem' %} {% if page_name == 'contest_detail_problem' %}