Skip to content

Commit dbd4cfe

Browse files
committed
wip: un-reviewed changes
1 parent a10b266 commit dbd4cfe

2 files changed

Lines changed: 133 additions & 14 deletions

File tree

packages/hydrooj/src/handler/record.ts

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ import { buildProjection, Time } from '../utils';
2424
import { ContestDetailBaseHandler } from './contest';
2525
import { postJudge } from './judge';
2626

27+
// FIXME: move this
28+
const isOwnOrTeammateRecord = async (domainId: string, rdoc: RecordDoc, uid: number) => (
29+
rdoc.uid === uid || (!!rdoc.contest && await contest.isSameTeam(domainId, rdoc.contest, rdoc.uid, uid))
30+
);
31+
2732
export class RecordListHandler extends ContestDetailBaseHandler {
2833
@param('page', Types.PositiveInt, true)
2934
@param('pid', Types.ProblemId, true)
@@ -53,16 +58,23 @@ export class RecordListHandler extends ContestDetailBaseHandler {
5358
if (udoc) q.uid = udoc._id;
5459
else invalid = true;
5560
}
56-
if (q.uid !== this.user._id) this.checkPerm(PERM.PERM_VIEW_RECORD);
61+
// Team: a member viewing the contest record list sees the whole team's submissions.
62+
let teamMembers: number[] | null = null;
63+
if (tid && this.team && (q.uid === undefined || q.uid === this.user._id)) {
64+
// this.tsdoc is already the team vuser's tsdoc (swapped in __prepare); no extra DB fetch needed
65+
teamMembers = this.tsdoc?.members?.length ? this.tsdoc.members : null;
66+
}
67+
if (q.uid !== this.user._id && !teamMembers) this.checkPerm(PERM.PERM_VIEW_RECORD);
5768
if (tid) {
5869
tdoc = await contest.get(domainId, tid);
5970
this.tdoc = tdoc;
6071
if (!tdoc) throw new ContestNotFoundError(domainId, pid);
6172
if (!contest.canShowScoreboard.call(this, tdoc, true)) throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
62-
if (!contest[q.uid === this.user._id ? 'canShowSelfRecord' : 'canShowRecord'].call(this, tdoc, true)) {
73+
const viewingSelf = q.uid === this.user._id || !!teamMembers;
74+
if (!contest[viewingSelf ? 'canShowSelfRecord' : 'canShowRecord'].call(this, tdoc, true)) {
6375
throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
6476
}
65-
if (!(await contest.getStatus(domainId, tid, this.user._id))?.attend) {
77+
if (!this.team && !(await contest.getStatus(domainId, tid, this.user._id))?.attend) {
6678
const name = tdoc.rule === 'homework'
6779
? "You haven't claimed this homework yet."
6880
: "You haven't attended this contest yet.";
@@ -89,6 +101,7 @@ export class RecordListHandler extends ContestDetailBaseHandler {
89101
delete q.contest;
90102
q._id = { $gt: Time.getObjectID(new Date(Date.now() - 10 * Time.week)) };
91103
}
104+
if (teamMembers && !all && !allDomain) q.uid = { $in: teamMembers };
92105
let cursor = record.getMulti(allDomain ? '' : domainId, q).sort('_id', -1);
93106
if (!full) cursor = cursor.project(buildProjection(record.PROJECTION_LIST));
94107
const limit = full ? 10 : system.get('pagination.record');
@@ -136,7 +149,7 @@ export class RecordDetailHandler extends ContestDetailBaseHandler {
136149
async prepare(domainId: string, rid: ObjectId) {
137150
this.rdoc = await record.get(domainId, rid);
138151
if (!this.rdoc) throw new RecordNotFoundError(rid);
139-
if (this.rdoc.uid !== this.user._id) this.checkPerm(PERM.PERM_VIEW_RECORD);
152+
if (!(await isOwnOrTeammateRecord(domainId, this.rdoc, this.user._id))) this.checkPerm(PERM.PERM_VIEW_RECORD);
140153
}
141154

142155
async download() {
@@ -171,8 +184,8 @@ export class RecordDetailHandler extends ContestDetailBaseHandler {
171184
this.tdoc = await contest.get(domainId, rdoc.contest);
172185
let canView = this.user.own(this.tdoc);
173186
canView ||= contest.canShowRecord.call(this, this.tdoc);
174-
canView ||= contest.canShowSelfRecord.call(this, this.tdoc, true) && rdoc.uid === this.user._id;
175-
if (!canView && rdoc.uid !== this.user._id) throw new PermissionError(rid);
187+
canView ||= contest.canShowSelfRecord.call(this, this.tdoc, true) && await isOwnOrTeammateRecord(domainId, rdoc, this.user._id);
188+
if (!canView && !(await isOwnOrTeammateRecord(domainId, rdoc, this.user._id))) throw new PermissionError(rid);
176189
canViewDetail = canView;
177190
this.args.tid = this.tdoc.docId;
178191
if (!this.user.own(this.tdoc) && !this.user.hasPerm(PERM.PERM_EDIT_CONTEST)) {
@@ -187,12 +200,13 @@ export class RecordDetailHandler extends ContestDetailBaseHandler {
187200
user.getById(domainId, rdoc.uid),
188201
]);
189202

190-
let canViewCode = rdoc.uid === this.user._id;
203+
let canViewCode = await isOwnOrTeammateRecord(domainId, rdoc, this.user._id);
191204
canViewCode ||= this.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE);
192205
canViewCode ||= this.user.hasPerm(PERM.PERM_READ_RECORD_CODE);
193206
canViewCode ||= this.user.hasPerm(PERM.PERM_READ_RECORD_CODE_ACCEPT) && self?.status === STATUS.STATUS_ACCEPTED;
194207
if (this.tdoc) {
195-
this.tsdoc = await contest.getStatus(domainId, this.tdoc.docId, this.user._id);
208+
const teamVuser = this.tdoc.allowTeam ? await contest.getTeamVuser(domainId, this.tdoc.docId, this.user._id) : null;
209+
this.tsdoc = await contest.getStatus(domainId, this.tdoc.docId, teamVuser ?? this.user._id);
196210
canViewCode ||= this.user.own(this.tdoc);
197211
if (this.tdoc.allowViewCode && contest.isDone(this.tdoc)) {
198212
canViewCode ||= !!this.tsdoc?.attend;
@@ -299,7 +313,12 @@ export class RecordMainConnectionHandler extends ConnectionHandler {
299313
else throw new UserNotFoundError(uidOrName);
300314
}
301315
}
302-
if (this.uid !== this.user._id) this.checkPerm(PERM.PERM_VIEW_RECORD);
316+
if (this.uid !== this.user._id) {
317+
const tdoc = this.tdoc;
318+
const sameTeam = !!tdoc && tdoc.allowTeam && typeof this.uid === 'number'
319+
&& await contest.isSameTeam(domainId, tdoc.docId, this.uid, this.user._id);
320+
if (!sameTeam) this.checkPerm(PERM.PERM_VIEW_RECORD);
321+
}
303322
if (pid) {
304323
const pdoc = await problem.get(domainId, pid);
305324
if (pdoc) this.pid = pdoc.docId;
@@ -336,8 +355,9 @@ export class RecordMainConnectionHandler extends ConnectionHandler {
336355
if (!rdoc.contest && this.tid) return;
337356
if (rdoc.contest && ![this.tid, '000000000000000000000000'].includes(rdoc.contest.toString())) return;
338357
if (this.tid && rdoc.contest?.toString() !== '0'.repeat(24)) {
339-
if (rdoc.uid !== this.user._id && !contest.canShowRecord.call(this, this.tdoc, true)) return;
340-
if (rdoc.uid === this.user._id && !contest.canShowSelfRecord.call(this, this.tdoc, true)) return;
358+
const own = await isOwnOrTeammateRecord(this.args.domainId, rdoc, this.user._id);
359+
if (!own && !contest.canShowRecord.call(this, this.tdoc, true)) return;
360+
if (own && !contest.canShowSelfRecord.call(this, this.tdoc, true)) return;
341361
}
342362
}
343363
}
@@ -397,7 +417,7 @@ export class RecordDetailConnectionHandler extends ConnectionHandler {
397417
this.tdoc = await contest.get(domainId, rdoc.contest);
398418
let canView = this.user.own(this.tdoc);
399419
canView ||= contest.canShowRecord.call(this, this.tdoc);
400-
canView ||= this.user._id === rdoc.uid && contest.canShowSelfRecord.call(this, this.tdoc);
420+
canView ||= (await isOwnOrTeammateRecord(domainId, rdoc, this.user._id)) && contest.canShowSelfRecord.call(this, this.tdoc);
401421
if (!canView) throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
402422
if (!this.user.own(this.tdoc) && !this.user.hasPerm(PERM.PERM_EDIT_CONTEST)) {
403423
this.applyProjection = true;
@@ -408,12 +428,12 @@ export class RecordDetailConnectionHandler extends ConnectionHandler {
408428
problem.getStatus(domainId, rdoc.pid, this.user._id),
409429
]);
410430

411-
this.canViewCode = rdoc.uid === this.user._id;
431+
this.canViewCode = await isOwnOrTeammateRecord(domainId, rdoc, this.user._id);
412432
this.canViewCode ||= this.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE);
413433
this.canViewCode ||= this.user.hasPerm(PERM.PERM_READ_RECORD_CODE);
414434
this.canViewCode ||= this.user.hasPerm(PERM.PERM_READ_RECORD_CODE_ACCEPT) && self?.status === STATUS.STATUS_ACCEPTED;
415435

416-
if (!rdoc.contest || this.user._id !== rdoc.uid) {
436+
if (!rdoc.contest || !(await isOwnOrTeammateRecord(domainId, rdoc, this.user._id))) {
417437
if (!problem.canViewBy(pdoc, this.user)) throw new PermissionError(PERM.PERM_VIEW_PROBLEM_HIDDEN);
418438
}
419439

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
{% extends "layout/basic.html" %}
2+
{% block content %}
3+
<div class="row">
4+
<div class="medium-9 columns">
5+
<div class="section">
6+
<div class="section__header">
7+
<h1 class="section__title">{{ _('Team Participation') }}</h1>
8+
</div>
9+
<div class="section__body">
10+
11+
<h3>{{ _('Create a Team') }}</h3>
12+
<form method="post" action="{{ url('contest_team') }}">
13+
<input type="hidden" name="operation" value="create">
14+
<input type="text" name="name" placeholder="{{ _('Team name (defaults to your username)') }}">
15+
<button type="submit" class="primary rounded button">{{ _('Create') }}</button>
16+
</form>
17+
18+
{% if invites.length %}
19+
<hr>
20+
<h3>{{ _('Invitations') }}</h3>
21+
<table class="data-table">
22+
<thead><tr><th>{{ _('Team') }}</th><th>{{ _('Action') }}</th></tr></thead>
23+
<tbody>
24+
{% for v in invites %}
25+
<tr>
26+
<td>{{ v.displayName }}</td>
27+
<td>
28+
<form method="post" action="{{ url('contest_team') }}" style="display:inline">
29+
<input type="hidden" name="operation" value="accept">
30+
<input type="hidden" name="vuid" value="{{ v._id }}">
31+
<button type="submit" class="button small">{{ _('Accept') }}</button>
32+
</form>
33+
<form method="post" action="{{ url('contest_team') }}" style="display:inline">
34+
<input type="hidden" name="operation" value="reject">
35+
<input type="hidden" name="vuid" value="{{ v._id }}">
36+
<button type="submit" class="button small">{{ _('Reject') }}</button>
37+
</form>
38+
</td>
39+
</tr>
40+
{% endfor %}
41+
</tbody>
42+
</table>
43+
{% endif %}
44+
45+
<hr>
46+
<h3>{{ _('My Teams') }}</h3>
47+
{% if not mine.length %}
48+
<p>{{ _('You are not in any team. Create one above, or ask a teammate to invite you by your User ID.') }}</p>
49+
{% endif %}
50+
{% for v in mine %}
51+
<div class="callout">
52+
<h4>{{ v.displayName }} <small>(ID {{ v._id }})</small></h4>
53+
54+
<form method="post" action="{{ url('contest_team') }}">
55+
<input type="hidden" name="operation" value="rename">
56+
<input type="hidden" name="vuid" value="{{ v._id }}">
57+
<input type="text" name="name" value="{{ v.displayName }}">
58+
<button type="submit" class="button small">{{ _('Rename') }}</button>
59+
</form>
60+
61+
<p><strong>{{ _('Members') }}</strong></p>
62+
<ul>
63+
{% for m in v.members %}
64+
<li>
65+
{{ udict[m].uname }}
66+
<form method="post" action="{{ url('contest_team') }}" style="display:inline">
67+
<input type="hidden" name="operation" value="leave">
68+
<input type="hidden" name="vuid" value="{{ v._id }}">
69+
<input type="hidden" name="uid" value="{{ m }}">
70+
<button type="submit" class="button small">{{ _('Remove') }}</button>
71+
</form>
72+
</li>
73+
{% endfor %}
74+
</ul>
75+
76+
{% if v.invite and v.invite.length %}
77+
<p><strong>{{ _('Pending Invites') }}:</strong> {{ v.invite.length }}</p>
78+
{% endif %}
79+
80+
<form method="post" action="{{ url('contest_team') }}">
81+
<input type="hidden" name="operation" value="invite">
82+
<input type="hidden" name="vuid" value="{{ v._id }}">
83+
<input type="number" name="uid" placeholder="{{ _('User ID to invite') }}">
84+
<button type="submit" class="button small">{{ _('Invite') }}</button>
85+
</form>
86+
87+
<form method="post" action="{{ url('contest_team') }}">
88+
<input type="hidden" name="operation" value="leave">
89+
<input type="hidden" name="vuid" value="{{ v._id }}">
90+
<button type="submit" class="button small">{{ _('Leave Team') }}</button>
91+
</form>
92+
</div>
93+
{% endfor %}
94+
95+
</div>
96+
</div>
97+
</div>
98+
</div>
99+
{% endblock %}

0 commit comments

Comments
 (0)