Skip to content

Commit daa06b5

Browse files
authored
Merge pull request #72 from codemate-oj/feat/contest-report
feat: contest report (backend)
2 parents ba0269c + f3bf0d0 commit daa06b5

1 file changed

Lines changed: 195 additions & 0 deletions

File tree

  • packages/codemate-plugin/plugins/contest-report
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import {
2+
ContestNotAttendedError,
3+
ContestScoreboardHiddenError,
4+
type Context,
5+
db,
6+
Handler,
7+
moment,
8+
NotAssignedError,
9+
ObjectId,
10+
param,
11+
PERM,
12+
type Tdoc,
13+
Types,
14+
} from 'hydrooj';
15+
import { GroupModel } from '../privilege-group/model';
16+
17+
const TYPE_CONTEST_REPORT = 33;
18+
19+
export interface ContestReportDoc {
20+
docType: 33;
21+
docId: ObjectId;
22+
tid: ObjectId;
23+
owner: number;
24+
date: string; // YYYYMMDD
25+
idByDate: number;
26+
}
27+
28+
declare module 'hydrooj' {
29+
interface DocType {
30+
[TYPE_CONTEST_REPORT]: ContestReportDoc;
31+
}
32+
}
33+
34+
const coll = db.collection('document');
35+
36+
class ContestReportHandler extends Handler {
37+
tdoc?: Tdoc;
38+
tsdoc?: any;
39+
40+
@param('tid', Types.ObjectId, true)
41+
async prepare(domainId: string, tid: ObjectId) {
42+
// Init contest data
43+
[this.tdoc, this.tsdoc] = await Promise.all([
44+
Hydro.model.contest.get(domainId, tid),
45+
Hydro.model.contest.getStatus(domainId, tid, this.user._id),
46+
]);
47+
if (this.tdoc.assign?.length && !this.user.own(this.tdoc) && !this.user.hasPerm(PERM.PERM_VIEW_HIDDEN_CONTEST)) {
48+
const groups = await GroupModel.list(domainId, this.user._id);
49+
if (
50+
!Set.intersection(
51+
this.tdoc.assign,
52+
groups.map((i) => i.name),
53+
).size
54+
) {
55+
throw new NotAssignedError('contest', tid);
56+
}
57+
}
58+
if (this.tdoc.duration && this.tsdoc?.startAt) {
59+
this.tsdoc.endAt = moment(this.tsdoc.startAt).add(this.tdoc.duration, 'hours').toDate();
60+
}
61+
62+
// Check permission
63+
if (!this.tsdoc?.attend) throw new ContestNotAttendedError(domainId, tid);
64+
}
65+
66+
@param('tid', Types.ObjectId)
67+
async get(domainId: string, tid: ObjectId) {
68+
let doc = await coll.findOne<ContestReportDoc>({ docType: TYPE_CONTEST_REPORT, tid, owner: this.user._id });
69+
70+
if (!doc) {
71+
const date = moment().format('YYYYMMDD');
72+
const newId =
73+
((await coll.findOne<ContestReportDoc>({ docType: TYPE_CONTEST_REPORT, date }, { sort: { idByDate: -1 } }))?.idByDate || 0) + 1;
74+
75+
await coll.insertOne({
76+
docType: TYPE_CONTEST_REPORT,
77+
docId: new ObjectId(),
78+
tid,
79+
owner: this.user._id,
80+
date,
81+
idByDate: newId,
82+
});
83+
84+
doc = await coll.findOne<ContestReportDoc>({ docType: TYPE_CONTEST_REPORT, tid, owner: this.user._id });
85+
}
86+
87+
const pdict = await Hydro.model.problem.getList(domainId, this.tdoc.pids, true, true, Hydro.model.problem.PROJECTION_LIST);
88+
const tdocProblemCategoryConfig = JSON.parse(this.tdoc.problemCategoryConfig || '[]');
89+
const pdocidsHasCategory = tdocProblemCategoryConfig.reduce((acc: number[], cur: any) => acc.concat(cur.problems.map((p) => p.docId)), []);
90+
const pdocidsDontHasCategory = this.tdoc.pids.filter((pid) => !pdocidsHasCategory.includes(pid));
91+
const categories = Object.fromEntries(tdocProblemCategoryConfig.map((c) => [c.name, c.problems]));
92+
93+
if ('未分类' in categories) {
94+
categories['未分类'] = categories['未分类'].concat(pdocidsDontHasCategory.map((pid) => ({ docId: pid, score: 100 })));
95+
} else {
96+
categories['未分类'] = pdocidsDontHasCategory.map((pid) => ({ docId: pid, score: 100 }));
97+
}
98+
99+
this.response.body = {
100+
report_id: `${doc.date}-${doc.idByDate}`,
101+
uname: this.user.uname,
102+
pdict,
103+
psdict: {},
104+
rdict: {},
105+
tdoc: this.tdoc,
106+
tsdoc: this.tsdoc,
107+
categories,
108+
};
109+
110+
this.response.body.psdict = this.tsdoc.detail || {};
111+
const psdocs: any[] = Object.values(this.response.body.psdict);
112+
113+
if (Hydro.model.contest.canShowSelfRecord.call(this, this.tdoc)) {
114+
[this.response.body.rdict, this.response.body.rdocs] = await Promise.all([
115+
Hydro.model.record.getList(
116+
domainId,
117+
psdocs.map((i: any) => i.rid),
118+
),
119+
await Hydro.model.record.getMulti(domainId, { contest: tid, uid: this.user._id }).sort({ _id: -1 }).toArray(),
120+
]);
121+
if (!this.user.own(this.tdoc) && !this.user.hasPerm(PERM.PERM_EDIT_CONTEST)) {
122+
this.response.body.rdocs = this.response.body.rdocs.map((rdoc) => Hydro.model.contest.applyProjection(this.tdoc, rdoc, this.user));
123+
for (const psdoc of psdocs) {
124+
this.response.body.rdict[psdoc.rid] = Hydro.model.contest.applyProjection(
125+
this.tdoc,
126+
this.response.body.rdict[psdoc.rid],
127+
this.user,
128+
);
129+
}
130+
}
131+
this.response.body.canViewRecord = true;
132+
} else {
133+
for (const i of psdocs) this.response.body.rdict[i.rid] = { _id: i.rid };
134+
}
135+
136+
this.response.body.categoriesScores = Object.fromEntries(
137+
Object.keys(categories).map((c) => [
138+
c,
139+
categories[c].reduce((acc: number, cur: any) => acc + (this.response.body.psdict[cur.docId]?.score || 0), 0),
140+
]),
141+
);
142+
}
143+
}
144+
145+
class ContestRankHandler extends Handler {
146+
tdoc?: Tdoc;
147+
tsdoc?: any;
148+
149+
@param('tid', Types.ObjectId, true)
150+
async prepare(domainId: string, tid: ObjectId) {
151+
// Init contest data
152+
[this.tdoc, this.tsdoc] = await Promise.all([
153+
Hydro.model.contest.get(domainId, tid),
154+
Hydro.model.contest.getStatus(domainId, tid, this.user._id),
155+
]);
156+
if (this.tdoc.assign?.length && !this.user.own(this.tdoc) && !this.user.hasPerm(PERM.PERM_VIEW_HIDDEN_CONTEST)) {
157+
const groups = await GroupModel.list(domainId, this.user._id);
158+
if (
159+
!Set.intersection(
160+
this.tdoc.assign,
161+
groups.map((i) => i.name),
162+
).size
163+
) {
164+
throw new NotAssignedError('contest', tid);
165+
}
166+
}
167+
if (this.tdoc.duration && this.tsdoc?.startAt) {
168+
this.tsdoc.endAt = moment(this.tsdoc.startAt).add(this.tdoc.duration, 'hours').toDate();
169+
}
170+
171+
// Check permission
172+
if (!this.tsdoc?.attend) throw new ContestNotAttendedError(domainId, tid);
173+
if (!Hydro.model.contest.canShowScoreboard.call(this, this.tdoc, true)) {
174+
throw new ContestScoreboardHiddenError(tid);
175+
}
176+
}
177+
178+
@param('tid', Types.ObjectId)
179+
async get(domainId: string, tid: ObjectId) {
180+
const tsdocsCursor = Hydro.model.contest.getMultiStatus(domainId, { docId: tid }).sort({ score: -1 });
181+
const rankedTsdocs = await Hydro.lib.rank(tsdocsCursor, (a, b) => a.score === b.score);
182+
const rows = [...(await Promise.all(rankedTsdocs.map(([rank, tsdoc]) => [rank, tsdoc.uid])))];
183+
const row = rows.find((r) => r[1] === this.user._id);
184+
185+
this.response.body = {
186+
rank: row ? row[0].toString() : '-1',
187+
total: rows.length,
188+
};
189+
}
190+
}
191+
192+
export async function apply(ctx: Context) {
193+
ctx.Route('contest_report', '/contest/:tid/report', ContestReportHandler, PERM.PERM_VIEW_CONTEST);
194+
ctx.Route('contest_rank', '/contest/:tid/rank', ContestRankHandler, PERM.PERM_VIEW_CONTEST);
195+
}

0 commit comments

Comments
 (0)