Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Testing Participation Type #1941

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dmoj/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ def paged_list_view(view, name):
path('/moss', contests.ContestMossView.as_view(), name='contest_moss'),
path('/moss/delete', contests.ContestMossDelete.as_view(), name='contest_moss_delete'),
path('/clone', contests.ContestClone.as_view(), name='contest_clone'),
path('/testing', contests.ContestTestingRanking.as_view(), name='contest_testing'),
path('/ranking/', contests.ContestRanking.as_view(), name='contest_ranking'),
path('/ranking/ajax', contests.contest_ranking_ajax, name='contest_ranking_ajax'),
path('/join', contests.ContestJoin.as_view(), name='contest_join'),
Expand Down
98 changes: 75 additions & 23 deletions judge/models/contest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Optional

from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
from django.db import models, transaction
Expand Down Expand Up @@ -216,6 +218,16 @@ def is_in_contest(self, user):
return profile and profile.current_contest is not None and profile.current_contest.contest == self
return False

def is_in_testing(self, user):
if user.is_authenticated:
profile = user.profile
return (
profile and profile.current_contest is not None and
profile.current_contest.contest == self and
profile.current_contest.testing
)
return False

def can_see_own_scoreboard(self, user):
if self.can_see_full_scoreboard(user):
return True
Expand Down Expand Up @@ -244,13 +256,48 @@ def can_see_full_scoreboard(self, user):
return True
return False

def has_completed_contest(self, user):
def can_see_own_testing_scoreboard(self, user) -> bool:
if self.can_see_full_testing_scoreboard(user):
return True

if self.is_in_testing(user) or self.has_completed_testing(user):
return True

return False

def can_see_full_testing_scoreboard(self, user) -> bool:
if user.profile.id in self.editor_ids:
return True

if user.profile.id not in self.tester_ids:
return False

# This means that after the contest has started, even if the scoreboard is HIDDEN,
# testers can see the testing scoreboard.
if self.started:
return True

if self.scoreboard_visibility == self.SCOREBOARD_VISIBLE:
return True

if self.scoreboard_visibility == self.SCOREBOARD_AFTER_PARTICIPATION and self.has_completed_testing(user):
return True

return False

def has_finished_participation(self, user, type: int) -> bool:
if user.is_authenticated:
participation = self.users.filter(virtual=ContestParticipation.LIVE, user=user.profile).first()
participation = self.users.filter(virtual=type, user=user.profile).first()
if participation and participation.ended:
return True
return False

def has_completed_contest(self, user):
return self.has_finished_participation(user, ContestParticipation.LIVE)

def has_completed_testing(self, user):
return self.has_finished_participation(user, ContestParticipation.TESTING)

@cached_property
def show_scoreboard(self):
if not self.started:
Expand Down Expand Up @@ -382,35 +429,35 @@ def access_check(self, user):
return
raise self.PrivateContest()

# Assumes the user can access, to avoid the cost again
def is_live_joinable_by(self, user):
if not self.started:
return False
def get_join_type(self, user) -> Optional[int]:
if self.ended:
return None # Virtual Join should not be LIVE or SPECTATE

if not user.is_authenticated:
return False
return None

if user.profile.id in self.editor_ids or user.profile.id in self.tester_ids:
return False
if user.profile.id in self.tester_ids:
if self.started or self.has_completed_testing(user):
return ContestParticipation.SPECTATE
return ContestParticipation.TESTING

if self.has_completed_contest(user):
return False
if user.profile.id in self.editor_ids:
return ContestParticipation.SPECTATE

if self.limit_join_organizations:
return self.join_organizations.filter(id__in=user.profile.organizations.all()).exists()
return True
if not self.started:
return None

# Also skips access check
def is_spectatable_by(self, user):
if not user.is_authenticated:
return False
if user.profile.id in self.spectator_ids:
return ContestParticipation.SPECTATE

if user.profile.id in self.editor_ids or user.profile.id in self.tester_ids:
return True
if (self.limit_join_organizations and
not self.join_organizations.filter(id__in=user.profile.organizations.all()).exists()):
return None

if self.limit_join_organizations:
return self.join_organizations.filter(id__in=user.profile.organizations.all()).exists()
return True
if self.has_completed_contest(user):
return ContestParticipation.SPECTATE

return ContestParticipation.LIVE

def is_accessible_by(self, user):
try:
Expand Down Expand Up @@ -486,6 +533,7 @@ class Meta:
class ContestParticipation(models.Model):
LIVE = 0
SPECTATE = -1
TESTING = -2

contest = models.ForeignKey(Contest, verbose_name=_('associated contest'), related_name='users', on_delete=CASCADE)
user = models.ForeignKey(Profile, verbose_name=_('user'), related_name='contest_history', on_delete=CASCADE)
Expand Down Expand Up @@ -530,6 +578,10 @@ def live(self):
def spectate(self):
return self.virtual == self.SPECTATE

@property
def testing(self):
return self.virtual == self.TESTING

@cached_property
def start(self):
contest = self.contest
Expand Down
Loading