core&ui: contest: tsdoc projection for contest

This commit is contained in:
undefined committed 2022-09-12 18:41:06 +08:00
1 parent 8f7d81ddca
commit dbe369d0c6
9 files changed
+57 -28

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "hydrooj", "name": "hydrooj",
"version": "3.17.5", "version": "3.17.6",
"bin": "bin/hydrooj.js", "bin": "bin/hydrooj.js",
"main": "src/loader", "main": "src/loader",
"module": "src/loader", "module": "src/loader",
+1 -1
View File
@@ -119,7 +119,7 @@ export class ContestDetailHandler extends ContestDetailBaseHandler {
this.response.template = 'contest_detail.html'; this.response.template = 'contest_detail.html';
const udict = await user.getList(domainId, [this.tdoc.owner]); const udict = await user.getList(domainId, [this.tdoc.owner]);
this.response.body = { this.response.body = {
tdoc: this.tdoc, tsdoc: this.tsdoc, udict, tdoc: this.tdoc, tsdoc: pick(this.tsdoc, ['attend', 'startAt']), udict,
}; };
if (!this.request.json) { if (!this.request.json) {
this.response.body.tdoc.content = this.response.body.tdoc.content this.response.body.tdoc.content = this.response.body.tdoc.content
+7 -5
View File
@@ -2,7 +2,7 @@ import AdmZip from 'adm-zip';
import { readFile, statSync } from 'fs-extra'; import { readFile, statSync } from 'fs-extra';
import { isBinaryFile } from 'isbinaryfile'; import { isBinaryFile } from 'isbinaryfile';
import { import {
escapeRegExp, flattenDeep, intersection, isSafeInteger, escapeRegExp, flattenDeep, intersection, isSafeInteger, pick,
} from 'lodash'; } from 'lodash';
import { FilterQuery, ObjectID } from 'mongodb'; import { FilterQuery, ObjectID } from 'mongodb';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
@@ -372,12 +372,12 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler {
this.response.body = { this.response.body = {
pdoc: this.pdoc, pdoc: this.pdoc,
udoc: this.udoc, udoc: this.udoc,
psdoc: this.psdoc, psdoc: tid ? null : this.psdoc,
title: this.pdoc.title, title: this.pdoc.title,
solutionCount: scnt, solutionCount: scnt,
discussionCount: dcnt, discussionCount: dcnt,
tdoc: this.tdoc, tdoc: this.tdoc,
tsdoc: this.tsdoc, tsdoc: pick(this.tsdoc, ['attend', 'startAt']),
}; };
this.response.template = 'problem_detail.html'; this.response.template = 'problem_detail.html';
this.UiContext.extraTitleContent = this.pdoc.title; this.UiContext.extraTitleContent = this.pdoc.title;
@@ -429,8 +429,10 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler {
if (this.psdoc?.rid) { if (this.psdoc?.rid) {
this.response.body.rdoc = await record.get(this.args.domainId, this.psdoc.rid); this.response.body.rdoc = await record.get(this.args.domainId, this.psdoc.rid);
} }
this.response.body.ctdocs = await contest.getRelated(this.args.domainId, this.pdoc.docId); [this.response.body.ctdocs, this.response.body.htdocs] = await Promise.all([
this.response.body.htdocs = await contest.getRelated(this.args.domainId, this.pdoc.docId, 'homework'); contest.getRelated(this.args.domainId, this.pdoc.docId),
contest.getRelated(this.args.domainId, this.pdoc.docId, 'homework'),
]);
} }
} }
+5 -1
View File
@@ -495,12 +495,16 @@ export interface ContestRule<T = any> {
TEXT: string; TEXT: string;
hidden?: boolean; hidden?: boolean;
check: (args: any) => any; check: (args: any) => any;
statusSort: any; statusSort: Record<string, 1 | -1>;
submitAfterAccept: boolean; submitAfterAccept: boolean;
showScoreboard: (tdoc: Tdoc<30>, now: Date) => boolean; showScoreboard: (tdoc: Tdoc<30>, now: Date) => boolean;
showSelfRecord: (tdoc: Tdoc<30>, now: Date) => boolean; showSelfRecord: (tdoc: Tdoc<30>, now: Date) => boolean;
showRecord: (tdoc: Tdoc<30>, now: Date) => boolean; showRecord: (tdoc: Tdoc<30>, now: Date) => boolean;
stat: (this: ContestRule<T>, tdoc: Tdoc<30>, journal: any[], ignoreLock?: boolean) => ContestStat & T; stat: (this: ContestRule<T>, tdoc: Tdoc<30>, journal: any[], ignoreLock?: boolean) => ContestStat & T;
scoreboardHeader: (
this: ContestRule<T>, isExport: boolean, _: (s: string) => string,
tdoc: Tdoc<30>, pdict: ProblemDict,
) => Promise<ScoreboardRow>;
scoreboard: ( scoreboard: (
this: ContestRule<T>, isExport: boolean, _: (s: string) => string, this: ContestRule<T>, isExport: boolean, _: (s: string) => string,
tdoc: Tdoc<30>, pdict: ProblemDict, cursor: Cursor<ContestStat & T>, page: number, tdoc: Tdoc<30>, pdict: ProblemDict, cursor: Cursor<ContestStat & T>, page: number,
+29 -17
View File
@@ -1,3 +1,4 @@
import { sumBy } from 'lodash';
import { FilterQuery, ObjectID } from 'mongodb'; import { FilterQuery, ObjectID } from 'mongodb';
import { Counter, formatSeconds, Time } from '@hydrooj/utils/lib/utils'; import { Counter, formatSeconds, Time } from '@hydrooj/utils/lib/utils';
import { import {
@@ -79,10 +80,7 @@ const acm = buildContestRule({
} }
return { accept, time, detail }; return { accept, time, detail };
}, },
async scoreboard(isExport, _, tdoc, pdict, cursor, page) { async scoreboardHeader(isExport, _, tdoc, pdict) {
const [rankedTsdocs, nPages] = await ranked(cursor, (a, b) => a.score === b.score && a.time === b.time, page);
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
const udict = await user.getList(tdoc.domainId, uids);
const columns: ScoreboardRow = [ const columns: ScoreboardRow = [
{ type: 'rank', value: '#' }, { type: 'rank', value: '#' },
{ type: 'user', value: _('User') }, { type: 'user', value: _('User') },
@@ -110,7 +108,12 @@ const acm = buildContestRule({
}); });
} }
} }
return columns;
},
async scoreboard(isExport, _, tdoc, pdict, cursor, page) {
const [rankedTsdocs, nPages] = await ranked(cursor, (a, b) => a.score === b.score && a.time === b.time, page);
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
const udict = await user.getList(tdoc.domainId, uids);
// Find first accept // Find first accept
const first = {}; const first = {};
const data = await document.collStatus.aggregate([ const data = await document.collStatus.aggregate([
@@ -130,6 +133,7 @@ const acm = buildContestRule({
]).toArray(); ]).toArray();
for (const t of data) first[t._id] = t.first.generationTime; for (const t of data) first[t._id] = t.first.generationTime;
const columns = await this.scoreboardHeader(isExport, _, tdoc, pdict);
const rows: ScoreboardRow[] = [columns]; const rows: ScoreboardRow[] = [columns];
for (const [rank, tsdoc] of rankedTsdocs) { for (const [rank, tsdoc] of rankedTsdocs) {
const tsddict: Record<number, AcmDetail> = {}; const tsddict: Record<number, AcmDetail> = {};
@@ -197,10 +201,7 @@ const oi = buildContestRule({
showScoreboard: (tdoc, now) => now > tdoc.endAt, showScoreboard: (tdoc, now) => now > tdoc.endAt,
showSelfRecord: (tdoc, now) => now > tdoc.endAt, showSelfRecord: (tdoc, now) => now > tdoc.endAt,
showRecord: (tdoc, now) => now > tdoc.endAt, showRecord: (tdoc, now) => now > tdoc.endAt,
async scoreboard(isExport, _, tdoc, pdict, cursor, page) { async scoreboardHeader(isExport, _, tdoc, pdict) {
const [rankedTsdocs, nPages] = await ranked(cursor, (a, b) => a.score === b.score, page);
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
const udict = await user.getList(tdoc.domainId, uids);
const columns: ScoreboardNode[] = [ const columns: ScoreboardNode[] = [
{ type: 'rank', value: '#' }, { type: 'rank', value: '#' },
{ type: 'user', value: _('User') }, { type: 'user', value: _('User') },
@@ -221,6 +222,12 @@ const oi = buildContestRule({
}); });
} }
} }
return columns;
},
async scoreboard(isExport, _, tdoc, pdict, cursor, page) {
const [rankedTsdocs, nPages] = await ranked(cursor, (a, b) => a.score === b.score, page);
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
const udict = await user.getList(tdoc.domainId, uids);
const psdict = {}; const psdict = {};
const first = {}; const first = {};
for (const pid of tdoc.pids) { for (const pid of tdoc.pids) {
@@ -243,6 +250,7 @@ const oi = buildContestRule({
} }
} }
} }
const columns = await this.scoreboardHeader(isExport, _, tdoc, pdict);
const rows: ScoreboardNode[][] = [columns]; const rows: ScoreboardNode[][] = [columns];
for (const [rank, tsdoc] of rankedTsdocs) { for (const [rank, tsdoc] of rankedTsdocs) {
const tsddict = {}; const tsddict = {};
@@ -250,7 +258,7 @@ const oi = buildContestRule({
if (!tsddict[item.pid] || tsddict[item.pid].score < item.score || this.submitAfterAccept) tsddict[item.pid] = item; if (!tsddict[item.pid] || tsddict[item.pid].score < item.score || this.submitAfterAccept) tsddict[item.pid] = item;
} }
const row: ScoreboardNode[] = [ const row: ScoreboardNode[] = [
{ type: 'string', value: rank.toString() }, { type: 'rank', value: rank.toString() },
{ type: 'user', value: udict[tsdoc.uid].uname, raw: tsdoc.uid }, { type: 'user', value: udict[tsdoc.uid].uname, raw: tsdoc.uid },
]; ];
if (isExport) row.push({ type: 'email', value: udict[tsdoc.uid].mail }); if (isExport) row.push({ type: 'email', value: udict[tsdoc.uid].mail });
@@ -337,8 +345,8 @@ const homework = buildContestRule({
}); });
} }
return { return {
score: Math.sum(detail.map((d) => d.score)), score: sumBy(detail, 'score'),
penaltyScore: Math.sum(detail.map((d) => d.penaltyScore)), penaltyScore: sumBy(detail, 'penaltyScore'),
time: Math.sum(detail.map((d) => d.time)), time: Math.sum(detail.map((d) => d.time)),
detail, detail,
}; };
@@ -346,10 +354,7 @@ const homework = buildContestRule({
showScoreboard: () => true, showScoreboard: () => true,
showSelfRecord: () => true, showSelfRecord: () => true,
showRecord: () => true, showRecord: () => true,
async scoreboard(isExport, _, tdoc, pdict, cursor, page) { async scoreboardHeader(isExport, _, tdoc, pdict) {
const [rankedTsdocs, nPages] = await ranked(cursor, (a, b) => a.score === b.score, page);
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
const udict = await user.getList(tdoc.domainId, uids);
const columns: ScoreboardNode[] = [ const columns: ScoreboardNode[] = [
{ type: 'rank', value: _('Rank') }, { type: 'rank', value: _('Rank') },
{ type: 'user', value: _('User') }, { type: 'user', value: _('User') },
@@ -384,6 +389,13 @@ const homework = buildContestRule({
}); });
} }
} }
return columns;
},
async scoreboard(isExport, _, tdoc, pdict, cursor, page) {
const [rankedTsdocs, nPages] = await ranked(cursor, (a, b) => a.score === b.score, page);
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
const udict = await user.getList(tdoc.domainId, uids);
const columns = await this.scoreboardHeader(isExport, _, tdoc, pdict);
const rows: ScoreboardRow[] = [columns]; const rows: ScoreboardRow[] = [columns];
for (const [rank, tsdoc] of rankedTsdocs) { for (const [rank, tsdoc] of rankedTsdocs) {
const tsddict = {}; const tsddict = {};
@@ -391,7 +403,7 @@ const homework = buildContestRule({
if (!tsddict[item.pid] || tsddict[item.pid].score <= item.score) tsddict[item.pid] = item; if (!tsddict[item.pid] || tsddict[item.pid].score <= item.score) tsddict[item.pid] = item;
} }
const row: ScoreboardRow = [ const row: ScoreboardRow = [
{ type: 'string', value: rank }, { type: 'rank', value: rank.toString() },
{ {
type: 'user', type: 'user',
value: udict[tsdoc.uid].uname, value: udict[tsdoc.uid].uname,
+11 -1
View File
@@ -2,8 +2,9 @@
import { FilterQuery } from 'mongodb'; import { FilterQuery } from 'mongodb';
import type { import type {
DomainDoc, ProblemStatusDoc, RecordDoc, DomainDoc, ProblemStatusDoc, RecordDoc,
Udoc, Tdoc, Udoc,
} from './interface'; } from './interface';
import * as contest from './model/contest';
import * as document from './model/document'; import * as document from './model/document';
import domain from './model/domain'; import domain from './model/domain';
import problem, { Field, ProblemDoc } from './model/problem'; import problem, { Field, ProblemDoc } from './model/problem';
@@ -20,6 +21,15 @@ export async function iterateAllUser(cb: (udoc: Udoc, current?: number, total?:
for (const i in udocs) await cb(udocs[i], +i, udocs.length); for (const i in udocs) await cb(udocs[i], +i, udocs.length);
} }
export async function iterateAllContest(cb: (tdoc: Tdoc) => Promise<any>) {
await iterateAllDomain(async (ddoc) => {
const tdocs = await contest.getMulti(ddoc._id, {}).toArray();
for (const tdoc of tdocs) {
await cb(tdoc);
}
});
}
export async function iterateAllPsdoc(filter: FilterQuery<ProblemStatusDoc>, cb: (psdoc: ProblemStatusDoc) => Promise<any>) { export async function iterateAllPsdoc(filter: FilterQuery<ProblemStatusDoc>, cb: (psdoc: ProblemStatusDoc) => Promise<any>) {
await iterateAllDomain(async ({ _id: domainId }) => { await iterateAllDomain(async ({ _id: domainId }) => {
const cursor = document.getMultiStatus(domainId, document.TYPE_PROBLEM, filter); const cursor = document.getMultiStatus(domainId, document.TYPE_PROBLEM, filter);
+1
View File
@@ -831,6 +831,7 @@ const scripts: UpgradeScript[] = [
return true; return true;
}, },
async function _67_78() { async function _67_78() {
const _FRESH_INSTALL_IGNORE = 1;
const rdocs = RecordModel.coll.find({ code: /^@@hydro_submission_file@@/ }); const rdocs = RecordModel.coll.find({ code: /^@@hydro_submission_file@@/ });
let rdoc; let rdoc;
while (rdoc = await rdocs.next()) { // eslint-disable-line while (rdoc = await rdocs.next()) { // eslint-disable-line
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@hydrooj/ui-default", "name": "@hydrooj/ui-default",
"version": "4.40.5", "version": "4.40.6",
"author": "undefined <i@undefined.moe>", "author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "hydro.js", "main": "hydro.js",
@@ -30,7 +30,7 @@
<tr> <tr>
{%- for column in rows[0] -%} {%- for column in rows[0] -%}
<th class="col--{{ column.type }}"> <th class="col--{{ column.type }}">
{% if column.type == 'problem' %} {% if column.type == 'problem' and column.raw %}
<a href="{{ url('problem_detail', pid=column.raw, query={tid:tdoc.docId}) }}" data-tooltip="{{ pdict[column.raw].title }}"> <a href="{{ url('problem_detail', pid=column.raw, query={tid:tdoc.docId}) }}" data-tooltip="{{ pdict[column.raw].title }}">
{{ column.value }}<br />{{ pdict[column.raw].nAccept }}/{{ pdict[column.raw].nSubmit }} {{ column.value }}<br />{{ pdict[column.raw].nAccept }}/{{ pdict[column.raw].nSubmit }}
</a> </a>