Reformat using black
This commit is contained in:
parent
efee4ad081
commit
a87fb49918
221 changed files with 19127 additions and 7310 deletions
|
@ -8,7 +8,13 @@ from django.utils.functional import cached_property
|
|||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from jsonfield import JSONField
|
||||
from lupa import LuaRuntime
|
||||
from moss import MOSS_LANG_C, MOSS_LANG_CC, MOSS_LANG_JAVA, MOSS_LANG_PYTHON, MOSS_LANG_PASCAL
|
||||
from moss import (
|
||||
MOSS_LANG_C,
|
||||
MOSS_LANG_CC,
|
||||
MOSS_LANG_JAVA,
|
||||
MOSS_LANG_PYTHON,
|
||||
MOSS_LANG_PASCAL,
|
||||
)
|
||||
|
||||
from judge import contest_format
|
||||
from judge.models.problem import Problem
|
||||
|
@ -16,22 +22,39 @@ from judge.models.profile import Organization, Profile
|
|||
from judge.models.submission import Submission
|
||||
from judge.ratings import rate_contest
|
||||
|
||||
__all__ = ['Contest', 'ContestTag', 'ContestParticipation', 'ContestProblem', 'ContestSubmission', 'Rating']
|
||||
__all__ = [
|
||||
"Contest",
|
||||
"ContestTag",
|
||||
"ContestParticipation",
|
||||
"ContestProblem",
|
||||
"ContestSubmission",
|
||||
"Rating",
|
||||
]
|
||||
|
||||
|
||||
class ContestTag(models.Model):
|
||||
color_validator = RegexValidator('^#(?:[A-Fa-f0-9]{3}){1,2}$', _('Invalid colour.'))
|
||||
color_validator = RegexValidator("^#(?:[A-Fa-f0-9]{3}){1,2}$", _("Invalid colour."))
|
||||
|
||||
name = models.CharField(max_length=20, verbose_name=_('tag name'), unique=True,
|
||||
validators=[RegexValidator(r'^[a-z-]+$', message=_('Lowercase letters and hyphens only.'))])
|
||||
color = models.CharField(max_length=7, verbose_name=_('tag colour'), validators=[color_validator])
|
||||
description = models.TextField(verbose_name=_('tag description'), blank=True)
|
||||
name = models.CharField(
|
||||
max_length=20,
|
||||
verbose_name=_("tag name"),
|
||||
unique=True,
|
||||
validators=[
|
||||
RegexValidator(
|
||||
r"^[a-z-]+$", message=_("Lowercase letters and hyphens only.")
|
||||
)
|
||||
],
|
||||
)
|
||||
color = models.CharField(
|
||||
max_length=7, verbose_name=_("tag colour"), validators=[color_validator]
|
||||
)
|
||||
description = models.TextField(verbose_name=_("tag description"), blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('contest_tag', args=[self.name])
|
||||
return reverse("contest_tag", args=[self.name])
|
||||
|
||||
@property
|
||||
def text_color(self, cache={}):
|
||||
|
@ -40,105 +63,232 @@ class ContestTag(models.Model):
|
|||
r, g, b = [ord(bytes.fromhex(i * 2)) for i in self.color[1:]]
|
||||
else:
|
||||
r, g, b = [i for i in bytes.fromhex(self.color[1:])]
|
||||
cache[self.color] = '#000' if 299 * r + 587 * g + 144 * b > 140000 else '#fff'
|
||||
cache[self.color] = (
|
||||
"#000" if 299 * r + 587 * g + 144 * b > 140000 else "#fff"
|
||||
)
|
||||
return cache[self.color]
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('contest tag')
|
||||
verbose_name_plural = _('contest tags')
|
||||
verbose_name = _("contest tag")
|
||||
verbose_name_plural = _("contest tags")
|
||||
|
||||
|
||||
class Contest(models.Model):
|
||||
SCOREBOARD_VISIBLE = 'V'
|
||||
SCOREBOARD_AFTER_CONTEST = 'C'
|
||||
SCOREBOARD_AFTER_PARTICIPATION = 'P'
|
||||
SCOREBOARD_VISIBLE = "V"
|
||||
SCOREBOARD_AFTER_CONTEST = "C"
|
||||
SCOREBOARD_AFTER_PARTICIPATION = "P"
|
||||
SCOREBOARD_VISIBILITY = (
|
||||
(SCOREBOARD_VISIBLE, _('Visible')),
|
||||
(SCOREBOARD_AFTER_CONTEST, _('Hidden for duration of contest')),
|
||||
(SCOREBOARD_AFTER_PARTICIPATION, _('Hidden for duration of participation')),
|
||||
(SCOREBOARD_VISIBLE, _("Visible")),
|
||||
(SCOREBOARD_AFTER_CONTEST, _("Hidden for duration of contest")),
|
||||
(SCOREBOARD_AFTER_PARTICIPATION, _("Hidden for duration of participation")),
|
||||
)
|
||||
key = models.CharField(max_length=20, verbose_name=_('contest id'), unique=True,
|
||||
validators=[RegexValidator('^[a-z0-9]+$', _('Contest id must be ^[a-z0-9]+$'))])
|
||||
name = models.CharField(max_length=100, verbose_name=_('contest name'), db_index=True)
|
||||
authors = models.ManyToManyField(Profile, help_text=_('These users will be able to edit the contest.'),
|
||||
related_name='authors+')
|
||||
curators = models.ManyToManyField(Profile, help_text=_('These users will be able to edit the contest, '
|
||||
'but will not be listed as authors.'),
|
||||
related_name='curators+', blank=True)
|
||||
testers = models.ManyToManyField(Profile, help_text=_('These users will be able to view the contest, '
|
||||
'but not edit it.'),
|
||||
blank=True, related_name='testers+')
|
||||
description = models.TextField(verbose_name=_('description'), blank=True)
|
||||
problems = models.ManyToManyField(Problem, verbose_name=_('problems'), through='ContestProblem')
|
||||
start_time = models.DateTimeField(verbose_name=_('start time'), db_index=True)
|
||||
end_time = models.DateTimeField(verbose_name=_('end time'), db_index=True)
|
||||
time_limit = models.DurationField(verbose_name=_('time limit'), blank=True, null=True,
|
||||
help_text=_('Format hh:mm:ss. For example, if you want a 2-hour contest, enter 02:00:00'))
|
||||
is_visible = models.BooleanField(verbose_name=_('publicly visible'), default=False,
|
||||
help_text=_('Should be set even for organization-private contests, where it '
|
||||
'determines whether the contest is visible to members of the '
|
||||
'specified organizations.'))
|
||||
is_rated = models.BooleanField(verbose_name=_('contest rated'), help_text=_('Whether this contest can be rated.'),
|
||||
default=False)
|
||||
scoreboard_visibility = models.CharField(verbose_name=_('scoreboard visibility'), default=SCOREBOARD_VISIBLE,
|
||||
max_length=1, help_text=_('Scoreboard visibility through the duration '
|
||||
'of the contest'), choices=SCOREBOARD_VISIBILITY)
|
||||
view_contest_scoreboard = models.ManyToManyField(Profile, verbose_name=_('view contest scoreboard'), blank=True,
|
||||
related_name='view_contest_scoreboard',
|
||||
help_text=_('These users will be able to view the scoreboard.'))
|
||||
use_clarifications = models.BooleanField(verbose_name=_('no comments'),
|
||||
help_text=_("Use clarification system instead of comments."),
|
||||
default=True)
|
||||
rating_floor = models.IntegerField(verbose_name=('rating floor'), help_text=_('Rating floor for contest'),
|
||||
null=True, blank=True)
|
||||
rating_ceiling = models.IntegerField(verbose_name=('rating ceiling'), help_text=_('Rating ceiling for contest'),
|
||||
null=True, blank=True)
|
||||
rate_all = models.BooleanField(verbose_name=_('rate all'), help_text=_('Rate all users who joined.'), default=False)
|
||||
rate_exclude = models.ManyToManyField(Profile, verbose_name=_('exclude from ratings'), blank=True,
|
||||
related_name='rate_exclude+')
|
||||
is_private = models.BooleanField(verbose_name=_('private to specific users'), default=False)
|
||||
private_contestants = models.ManyToManyField(Profile, blank=True, verbose_name=_('private contestants'),
|
||||
help_text=_('If private, only these users may see the contest'),
|
||||
related_name='private_contestants+')
|
||||
hide_problem_tags = models.BooleanField(verbose_name=_('hide problem tags'),
|
||||
help_text=_('Whether problem tags should be hidden by default.'),
|
||||
default=True)
|
||||
run_pretests_only = models.BooleanField(verbose_name=_('run pretests only'),
|
||||
help_text=_('Whether judges should grade pretests only, versus all '
|
||||
'testcases. Commonly set during a contest, then unset '
|
||||
'prior to rejudging user submissions when the contest ends.'),
|
||||
default=False)
|
||||
is_organization_private = models.BooleanField(verbose_name=_('private to organizations'), default=False)
|
||||
organizations = models.ManyToManyField(Organization, blank=True, verbose_name=_('organizations'),
|
||||
help_text=_('If private, only these organizations may see the contest'))
|
||||
og_image = models.CharField(verbose_name=_('OpenGraph image'), default='', max_length=150, blank=True)
|
||||
logo_override_image = models.CharField(verbose_name=_('Logo override image'), default='', max_length=150,
|
||||
blank=True,
|
||||
help_text=_('This image will replace the default site logo for users '
|
||||
'inside the contest.'))
|
||||
tags = models.ManyToManyField(ContestTag, verbose_name=_('contest tags'), blank=True, related_name='contests')
|
||||
user_count = models.IntegerField(verbose_name=_('the amount of live participants'), default=0)
|
||||
summary = models.TextField(blank=True, verbose_name=_('contest summary'),
|
||||
help_text=_('Plain-text, shown in meta description tag, e.g. for social media.'))
|
||||
access_code = models.CharField(verbose_name=_('access code'), blank=True, default='', max_length=255,
|
||||
help_text=_('An optional code to prompt contestants before they are allowed '
|
||||
'to join the contest. Leave it blank to disable.'))
|
||||
banned_users = models.ManyToManyField(Profile, verbose_name=_('personae non gratae'), blank=True,
|
||||
help_text=_('Bans the selected users from joining this contest.'))
|
||||
format_name = models.CharField(verbose_name=_('contest format'), default='default', max_length=32,
|
||||
choices=contest_format.choices(), help_text=_('The contest format module to use.'))
|
||||
format_config = JSONField(verbose_name=_('contest format configuration'), null=True, blank=True,
|
||||
help_text=_('A JSON object to serve as the configuration for the chosen contest format '
|
||||
'module. Leave empty to use None. Exact format depends on the contest format '
|
||||
'selected.'))
|
||||
problem_label_script = models.TextField(verbose_name='contest problem label script', blank=True,
|
||||
help_text='A custom Lua function to generate problem labels. Requires a '
|
||||
'single function with an integer parameter, the zero-indexed '
|
||||
'contest problem index, and returns a string, the label.')
|
||||
points_precision = models.IntegerField(verbose_name=_('precision points'), default=2,
|
||||
validators=[MinValueValidator(0), MaxValueValidator(10)],
|
||||
help_text=_('Number of digits to round points to.'))
|
||||
|
||||
key = models.CharField(
|
||||
max_length=20,
|
||||
verbose_name=_("contest id"),
|
||||
unique=True,
|
||||
validators=[RegexValidator("^[a-z0-9]+$", _("Contest id must be ^[a-z0-9]+$"))],
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=100, verbose_name=_("contest name"), db_index=True
|
||||
)
|
||||
authors = models.ManyToManyField(
|
||||
Profile,
|
||||
help_text=_("These users will be able to edit the contest."),
|
||||
related_name="authors+",
|
||||
)
|
||||
curators = models.ManyToManyField(
|
||||
Profile,
|
||||
help_text=_(
|
||||
"These users will be able to edit the contest, "
|
||||
"but will not be listed as authors."
|
||||
),
|
||||
related_name="curators+",
|
||||
blank=True,
|
||||
)
|
||||
testers = models.ManyToManyField(
|
||||
Profile,
|
||||
help_text=_(
|
||||
"These users will be able to view the contest, " "but not edit it."
|
||||
),
|
||||
blank=True,
|
||||
related_name="testers+",
|
||||
)
|
||||
description = models.TextField(verbose_name=_("description"), blank=True)
|
||||
problems = models.ManyToManyField(
|
||||
Problem, verbose_name=_("problems"), through="ContestProblem"
|
||||
)
|
||||
start_time = models.DateTimeField(verbose_name=_("start time"), db_index=True)
|
||||
end_time = models.DateTimeField(verbose_name=_("end time"), db_index=True)
|
||||
time_limit = models.DurationField(
|
||||
verbose_name=_("time limit"),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_(
|
||||
"Format hh:mm:ss. For example, if you want a 2-hour contest, enter 02:00:00"
|
||||
),
|
||||
)
|
||||
is_visible = models.BooleanField(
|
||||
verbose_name=_("publicly visible"),
|
||||
default=False,
|
||||
help_text=_(
|
||||
"Should be set even for organization-private contests, where it "
|
||||
"determines whether the contest is visible to members of the "
|
||||
"specified organizations."
|
||||
),
|
||||
)
|
||||
is_rated = models.BooleanField(
|
||||
verbose_name=_("contest rated"),
|
||||
help_text=_("Whether this contest can be rated."),
|
||||
default=False,
|
||||
)
|
||||
scoreboard_visibility = models.CharField(
|
||||
verbose_name=_("scoreboard visibility"),
|
||||
default=SCOREBOARD_VISIBLE,
|
||||
max_length=1,
|
||||
help_text=_("Scoreboard visibility through the duration " "of the contest"),
|
||||
choices=SCOREBOARD_VISIBILITY,
|
||||
)
|
||||
view_contest_scoreboard = models.ManyToManyField(
|
||||
Profile,
|
||||
verbose_name=_("view contest scoreboard"),
|
||||
blank=True,
|
||||
related_name="view_contest_scoreboard",
|
||||
help_text=_("These users will be able to view the scoreboard."),
|
||||
)
|
||||
use_clarifications = models.BooleanField(
|
||||
verbose_name=_("no comments"),
|
||||
help_text=_("Use clarification system instead of comments."),
|
||||
default=True,
|
||||
)
|
||||
rating_floor = models.IntegerField(
|
||||
verbose_name=("rating floor"),
|
||||
help_text=_("Rating floor for contest"),
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
rating_ceiling = models.IntegerField(
|
||||
verbose_name=("rating ceiling"),
|
||||
help_text=_("Rating ceiling for contest"),
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
rate_all = models.BooleanField(
|
||||
verbose_name=_("rate all"),
|
||||
help_text=_("Rate all users who joined."),
|
||||
default=False,
|
||||
)
|
||||
rate_exclude = models.ManyToManyField(
|
||||
Profile,
|
||||
verbose_name=_("exclude from ratings"),
|
||||
blank=True,
|
||||
related_name="rate_exclude+",
|
||||
)
|
||||
is_private = models.BooleanField(
|
||||
verbose_name=_("private to specific users"), default=False
|
||||
)
|
||||
private_contestants = models.ManyToManyField(
|
||||
Profile,
|
||||
blank=True,
|
||||
verbose_name=_("private contestants"),
|
||||
help_text=_("If private, only these users may see the contest"),
|
||||
related_name="private_contestants+",
|
||||
)
|
||||
hide_problem_tags = models.BooleanField(
|
||||
verbose_name=_("hide problem tags"),
|
||||
help_text=_("Whether problem tags should be hidden by default."),
|
||||
default=True,
|
||||
)
|
||||
run_pretests_only = models.BooleanField(
|
||||
verbose_name=_("run pretests only"),
|
||||
help_text=_(
|
||||
"Whether judges should grade pretests only, versus all "
|
||||
"testcases. Commonly set during a contest, then unset "
|
||||
"prior to rejudging user submissions when the contest ends."
|
||||
),
|
||||
default=False,
|
||||
)
|
||||
is_organization_private = models.BooleanField(
|
||||
verbose_name=_("private to organizations"), default=False
|
||||
)
|
||||
organizations = models.ManyToManyField(
|
||||
Organization,
|
||||
blank=True,
|
||||
verbose_name=_("organizations"),
|
||||
help_text=_("If private, only these organizations may see the contest"),
|
||||
)
|
||||
og_image = models.CharField(
|
||||
verbose_name=_("OpenGraph image"), default="", max_length=150, blank=True
|
||||
)
|
||||
logo_override_image = models.CharField(
|
||||
verbose_name=_("Logo override image"),
|
||||
default="",
|
||||
max_length=150,
|
||||
blank=True,
|
||||
help_text=_(
|
||||
"This image will replace the default site logo for users "
|
||||
"inside the contest."
|
||||
),
|
||||
)
|
||||
tags = models.ManyToManyField(
|
||||
ContestTag, verbose_name=_("contest tags"), blank=True, related_name="contests"
|
||||
)
|
||||
user_count = models.IntegerField(
|
||||
verbose_name=_("the amount of live participants"), default=0
|
||||
)
|
||||
summary = models.TextField(
|
||||
blank=True,
|
||||
verbose_name=_("contest summary"),
|
||||
help_text=_(
|
||||
"Plain-text, shown in meta description tag, e.g. for social media."
|
||||
),
|
||||
)
|
||||
access_code = models.CharField(
|
||||
verbose_name=_("access code"),
|
||||
blank=True,
|
||||
default="",
|
||||
max_length=255,
|
||||
help_text=_(
|
||||
"An optional code to prompt contestants before they are allowed "
|
||||
"to join the contest. Leave it blank to disable."
|
||||
),
|
||||
)
|
||||
banned_users = models.ManyToManyField(
|
||||
Profile,
|
||||
verbose_name=_("personae non gratae"),
|
||||
blank=True,
|
||||
help_text=_("Bans the selected users from joining this contest."),
|
||||
)
|
||||
format_name = models.CharField(
|
||||
verbose_name=_("contest format"),
|
||||
default="default",
|
||||
max_length=32,
|
||||
choices=contest_format.choices(),
|
||||
help_text=_("The contest format module to use."),
|
||||
)
|
||||
format_config = JSONField(
|
||||
verbose_name=_("contest format configuration"),
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_(
|
||||
"A JSON object to serve as the configuration for the chosen contest format "
|
||||
"module. Leave empty to use None. Exact format depends on the contest format "
|
||||
"selected."
|
||||
),
|
||||
)
|
||||
problem_label_script = models.TextField(
|
||||
verbose_name="contest problem label script",
|
||||
blank=True,
|
||||
help_text="A custom Lua function to generate problem labels. Requires a "
|
||||
"single function with an integer parameter, the zero-indexed "
|
||||
"contest problem index, and returns a string, the label.",
|
||||
)
|
||||
points_precision = models.IntegerField(
|
||||
verbose_name=_("precision points"),
|
||||
default=2,
|
||||
validators=[MinValueValidator(0), MaxValueValidator(10)],
|
||||
help_text=_("Number of digits to round points to."),
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def format_class(self):
|
||||
return contest_format.formats[self.format_name]
|
||||
|
@ -151,13 +301,20 @@ class Contest(models.Model):
|
|||
def get_label_for_problem(self):
|
||||
def DENY_ALL(obj, attr_name, is_setting):
|
||||
raise AttributeError()
|
||||
lua = LuaRuntime(attribute_filter=DENY_ALL, register_eval=False, register_builtins=False)
|
||||
return lua.eval(self.problem_label_script or self.format.get_contest_problem_label_script())
|
||||
|
||||
lua = LuaRuntime(
|
||||
attribute_filter=DENY_ALL, register_eval=False, register_builtins=False
|
||||
)
|
||||
return lua.eval(
|
||||
self.problem_label_script or self.format.get_contest_problem_label_script()
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
# Django will complain if you didn't fill in start_time or end_time, so we don't have to.
|
||||
if self.start_time and self.end_time and self.start_time >= self.end_time:
|
||||
raise ValidationError('What is this? A contest that ended before it starts?')
|
||||
raise ValidationError(
|
||||
"What is this? A contest that ended before it starts?"
|
||||
)
|
||||
self.format_class.validate(self.format_config)
|
||||
|
||||
try:
|
||||
|
@ -165,15 +322,21 @@ class Contest(models.Model):
|
|||
# so test it to see if the script returns a valid label.
|
||||
label = self.get_label_for_problem(0)
|
||||
except Exception as e:
|
||||
raise ValidationError('Contest problem label script: %s' % e)
|
||||
raise ValidationError("Contest problem label script: %s" % e)
|
||||
else:
|
||||
if not isinstance(label, str):
|
||||
raise ValidationError('Contest problem label script: script should return a string.')
|
||||
raise ValidationError(
|
||||
"Contest problem label script: script should return a string."
|
||||
)
|
||||
|
||||
def is_in_contest(self, user):
|
||||
if user.is_authenticated:
|
||||
profile = user.profile
|
||||
return profile and profile.current_contest is not None and profile.current_contest.contest == self
|
||||
return (
|
||||
profile
|
||||
and profile.current_contest is not None
|
||||
and profile.current_contest.contest == self
|
||||
)
|
||||
return False
|
||||
|
||||
def can_see_own_scoreboard(self, user):
|
||||
|
@ -190,19 +353,26 @@ class Contest(models.Model):
|
|||
return True
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
if user.has_perm('judge.see_private_contest') or user.has_perm('judge.edit_all_contest'):
|
||||
if user.has_perm("judge.see_private_contest") or user.has_perm(
|
||||
"judge.edit_all_contest"
|
||||
):
|
||||
return True
|
||||
if user.profile.id in self.editor_ids:
|
||||
return True
|
||||
if self.view_contest_scoreboard.filter(id=user.profile.id).exists():
|
||||
return True
|
||||
if self.scoreboard_visibility == self.SCOREBOARD_AFTER_PARTICIPATION and self.has_completed_contest(user):
|
||||
if (
|
||||
self.scoreboard_visibility == self.SCOREBOARD_AFTER_PARTICIPATION
|
||||
and self.has_completed_contest(user)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_completed_contest(self, user):
|
||||
if user.is_authenticated:
|
||||
participation = self.users.filter(virtual=ContestParticipation.LIVE, user=user.profile).first()
|
||||
participation = self.users.filter(
|
||||
virtual=ContestParticipation.LIVE, user=user.profile
|
||||
).first()
|
||||
if participation and participation.ended:
|
||||
return True
|
||||
return False
|
||||
|
@ -211,8 +381,11 @@ class Contest(models.Model):
|
|||
def show_scoreboard(self):
|
||||
if not self.can_join:
|
||||
return False
|
||||
if (self.scoreboard_visibility in (self.SCOREBOARD_AFTER_CONTEST, self.SCOREBOARD_AFTER_PARTICIPATION) and
|
||||
not self.ended):
|
||||
if (
|
||||
self.scoreboard_visibility
|
||||
in (self.SCOREBOARD_AFTER_CONTEST, self.SCOREBOARD_AFTER_PARTICIPATION)
|
||||
and not self.ended
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
@ -249,22 +422,29 @@ class Contest(models.Model):
|
|||
|
||||
@cached_property
|
||||
def author_ids(self):
|
||||
return Contest.authors.through.objects.filter(contest=self).values_list('profile_id', flat=True)
|
||||
return Contest.authors.through.objects.filter(contest=self).values_list(
|
||||
"profile_id", flat=True
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def editor_ids(self):
|
||||
return self.author_ids.union(
|
||||
Contest.curators.through.objects.filter(contest=self).values_list('profile_id', flat=True))
|
||||
Contest.curators.through.objects.filter(contest=self).values_list(
|
||||
"profile_id", flat=True
|
||||
)
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def tester_ids(self):
|
||||
return Contest.testers.through.objects.filter(contest=self).values_list('profile_id', flat=True)
|
||||
return Contest.testers.through.objects.filter(contest=self).values_list(
|
||||
"profile_id", flat=True
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('contest_view', args=(self.key,))
|
||||
return reverse("contest_view", args=(self.key,))
|
||||
|
||||
def update_user_count(self):
|
||||
self.user_count = self.users.filter(virtual=0).count()
|
||||
|
@ -289,7 +469,9 @@ class Contest(models.Model):
|
|||
return
|
||||
|
||||
# If the user can view or edit all contests
|
||||
if user.has_perm('judge.see_private_contest') or user.has_perm('judge.edit_all_contest'):
|
||||
if user.has_perm("judge.see_private_contest") or user.has_perm(
|
||||
"judge.edit_all_contest"
|
||||
):
|
||||
return
|
||||
|
||||
# User is organizer or curator for contest
|
||||
|
@ -310,14 +492,16 @@ class Contest(models.Model):
|
|||
|
||||
if self.view_contest_scoreboard.filter(id=user.profile.id).exists():
|
||||
return
|
||||
|
||||
in_org = self.organizations.filter(id__in=user.profile.organizations.all()).exists()
|
||||
|
||||
in_org = self.organizations.filter(
|
||||
id__in=user.profile.organizations.all()
|
||||
).exists()
|
||||
in_users = self.private_contestants.filter(id=user.profile.id).exists()
|
||||
|
||||
if not self.is_private and self.is_organization_private:
|
||||
if in_org:
|
||||
return
|
||||
raise self.PrivateContest()
|
||||
raise self.PrivateContest()
|
||||
|
||||
if self.is_private and not self.is_organization_private:
|
||||
if in_users:
|
||||
|
@ -339,11 +523,14 @@ class Contest(models.Model):
|
|||
|
||||
def is_editable_by(self, user):
|
||||
# If the user can edit all contests
|
||||
if user.has_perm('judge.edit_all_contest'):
|
||||
if user.has_perm("judge.edit_all_contest"):
|
||||
return True
|
||||
|
||||
# If the user is a contest organizer or curator
|
||||
if user.has_perm('judge.edit_own_contest') and user.profile.id in self.editor_ids:
|
||||
if (
|
||||
user.has_perm("judge.edit_own_contest")
|
||||
and user.profile.id in self.editor_ids
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
@ -351,19 +538,39 @@ class Contest(models.Model):
|
|||
@classmethod
|
||||
def get_visible_contests(cls, user):
|
||||
if not user.is_authenticated:
|
||||
return cls.objects.filter(is_visible=True, is_organization_private=False, is_private=False) \
|
||||
.defer('description').distinct()
|
||||
return (
|
||||
cls.objects.filter(
|
||||
is_visible=True, is_organization_private=False, is_private=False
|
||||
)
|
||||
.defer("description")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
queryset = cls.objects.defer('description')
|
||||
if not (user.has_perm('judge.see_private_contest') or user.has_perm('judge.edit_all_contest')):
|
||||
queryset = cls.objects.defer("description")
|
||||
if not (
|
||||
user.has_perm("judge.see_private_contest")
|
||||
or user.has_perm("judge.edit_all_contest")
|
||||
):
|
||||
q = Q(is_visible=True)
|
||||
q &= (
|
||||
Q(view_contest_scoreboard=user.profile) |
|
||||
Q(is_organization_private=False, is_private=False) |
|
||||
Q(is_organization_private=False, is_private=True, private_contestants=user.profile) |
|
||||
Q(is_organization_private=True, is_private=False, organizations__in=user.profile.organizations.all()) |
|
||||
Q(is_organization_private=True, is_private=True, organizations__in=user.profile.organizations.all(),
|
||||
private_contestants=user.profile)
|
||||
Q(view_contest_scoreboard=user.profile)
|
||||
| Q(is_organization_private=False, is_private=False)
|
||||
| Q(
|
||||
is_organization_private=False,
|
||||
is_private=True,
|
||||
private_contestants=user.profile,
|
||||
)
|
||||
| Q(
|
||||
is_organization_private=True,
|
||||
is_private=False,
|
||||
organizations__in=user.profile.organizations.all(),
|
||||
)
|
||||
| Q(
|
||||
is_organization_private=True,
|
||||
is_private=True,
|
||||
organizations__in=user.profile.organizations.all(),
|
||||
private_contestants=user.profile,
|
||||
)
|
||||
)
|
||||
|
||||
q |= Q(authors=user.profile)
|
||||
|
@ -373,51 +580,75 @@ class Contest(models.Model):
|
|||
return queryset.distinct()
|
||||
|
||||
def rate(self):
|
||||
Rating.objects.filter(contest__end_time__range=(self.end_time, self._now)).delete()
|
||||
Rating.objects.filter(
|
||||
contest__end_time__range=(self.end_time, self._now)
|
||||
).delete()
|
||||
for contest in Contest.objects.filter(
|
||||
is_rated=True, end_time__range=(self.end_time, self._now),
|
||||
).order_by('end_time'):
|
||||
is_rated=True,
|
||||
end_time__range=(self.end_time, self._now),
|
||||
).order_by("end_time"):
|
||||
rate_contest(contest)
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('see_private_contest', _('See private contests')),
|
||||
('edit_own_contest', _('Edit own contests')),
|
||||
('edit_all_contest', _('Edit all contests')),
|
||||
('clone_contest', _('Clone contest')),
|
||||
('moss_contest', _('MOSS contest')),
|
||||
('contest_rating', _('Rate contests')),
|
||||
('contest_access_code', _('Contest access codes')),
|
||||
('create_private_contest', _('Create private contests')),
|
||||
('change_contest_visibility', _('Change contest visibility')),
|
||||
('contest_problem_label', _('Edit contest problem label script')),
|
||||
("see_private_contest", _("See private contests")),
|
||||
("edit_own_contest", _("Edit own contests")),
|
||||
("edit_all_contest", _("Edit all contests")),
|
||||
("clone_contest", _("Clone contest")),
|
||||
("moss_contest", _("MOSS contest")),
|
||||
("contest_rating", _("Rate contests")),
|
||||
("contest_access_code", _("Contest access codes")),
|
||||
("create_private_contest", _("Create private contests")),
|
||||
("change_contest_visibility", _("Change contest visibility")),
|
||||
("contest_problem_label", _("Edit contest problem label script")),
|
||||
)
|
||||
verbose_name = _('contest')
|
||||
verbose_name_plural = _('contests')
|
||||
verbose_name = _("contest")
|
||||
verbose_name_plural = _("contests")
|
||||
|
||||
|
||||
class ContestParticipation(models.Model):
|
||||
LIVE = 0
|
||||
SPECTATE = -1
|
||||
|
||||
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)
|
||||
real_start = models.DateTimeField(verbose_name=_('start time'), default=timezone.now, db_column='start')
|
||||
score = models.FloatField(verbose_name=_('score'), default=0, db_index=True)
|
||||
cumtime = models.PositiveIntegerField(verbose_name=_('cumulative time'), default=0)
|
||||
is_disqualified = models.BooleanField(verbose_name=_('is disqualified'), default=False,
|
||||
help_text=_('Whether this participation is disqualified.'))
|
||||
tiebreaker = models.FloatField(verbose_name=_('tie-breaking field'), default=0.0)
|
||||
virtual = models.IntegerField(verbose_name=_('virtual participation id'), default=LIVE,
|
||||
help_text=_('0 means non-virtual, otherwise the n-th virtual participation.'))
|
||||
format_data = JSONField(verbose_name=_('contest format specific data'), null=True, blank=True)
|
||||
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,
|
||||
)
|
||||
real_start = models.DateTimeField(
|
||||
verbose_name=_("start time"), default=timezone.now, db_column="start"
|
||||
)
|
||||
score = models.FloatField(verbose_name=_("score"), default=0, db_index=True)
|
||||
cumtime = models.PositiveIntegerField(verbose_name=_("cumulative time"), default=0)
|
||||
is_disqualified = models.BooleanField(
|
||||
verbose_name=_("is disqualified"),
|
||||
default=False,
|
||||
help_text=_("Whether this participation is disqualified."),
|
||||
)
|
||||
tiebreaker = models.FloatField(verbose_name=_("tie-breaking field"), default=0.0)
|
||||
virtual = models.IntegerField(
|
||||
verbose_name=_("virtual participation id"),
|
||||
default=LIVE,
|
||||
help_text=_("0 means non-virtual, otherwise the n-th virtual participation."),
|
||||
)
|
||||
format_data = JSONField(
|
||||
verbose_name=_("contest format specific data"), null=True, blank=True
|
||||
)
|
||||
|
||||
def recompute_results(self):
|
||||
with transaction.atomic():
|
||||
self.contest.format.update_participation(self)
|
||||
if self.is_disqualified:
|
||||
self.score = -9999
|
||||
self.save(update_fields=['score'])
|
||||
self.save(update_fields=["score"])
|
||||
|
||||
recompute_results.alters_data = True
|
||||
|
||||
def set_disqualified(self, disqualified):
|
||||
|
@ -431,6 +662,7 @@ class ContestParticipation(models.Model):
|
|||
self.contest.banned_users.add(self.user)
|
||||
else:
|
||||
self.contest.banned_users.remove(self.user)
|
||||
|
||||
set_disqualified.alters_data = True
|
||||
|
||||
@property
|
||||
|
@ -444,7 +676,11 @@ class ContestParticipation(models.Model):
|
|||
@cached_property
|
||||
def start(self):
|
||||
contest = self.contest
|
||||
return contest.start_time if contest.time_limit is None and (self.live or self.spectate) else self.real_start
|
||||
return (
|
||||
contest.start_time
|
||||
if contest.time_limit is None and (self.live or self.spectate)
|
||||
else self.real_start
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def end_time(self):
|
||||
|
@ -456,8 +692,11 @@ class ContestParticipation(models.Model):
|
|||
return self.real_start + contest.time_limit
|
||||
else:
|
||||
return self.real_start + (contest.end_time - contest.start_time)
|
||||
return contest.end_time if contest.time_limit is None else \
|
||||
min(self.real_start + contest.time_limit, contest.end_time)
|
||||
return (
|
||||
contest.end_time
|
||||
if contest.time_limit is None
|
||||
else min(self.real_start + contest.time_limit, contest.end_time)
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def _now(self):
|
||||
|
@ -476,88 +715,140 @@ class ContestParticipation(models.Model):
|
|||
|
||||
def __str__(self):
|
||||
if self.spectate:
|
||||
return gettext('%s spectating in %s') % (self.user.username, self.contest.name)
|
||||
return gettext("%s spectating in %s") % (
|
||||
self.user.username,
|
||||
self.contest.name,
|
||||
)
|
||||
if self.virtual:
|
||||
return gettext('%s in %s, v%d') % (self.user.username, self.contest.name, self.virtual)
|
||||
return gettext('%s in %s') % (self.user.username, self.contest.name)
|
||||
return gettext("%s in %s, v%d") % (
|
||||
self.user.username,
|
||||
self.contest.name,
|
||||
self.virtual,
|
||||
)
|
||||
return gettext("%s in %s") % (self.user.username, self.contest.name)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('contest participation')
|
||||
verbose_name_plural = _('contest participations')
|
||||
verbose_name = _("contest participation")
|
||||
verbose_name_plural = _("contest participations")
|
||||
|
||||
unique_together = ('contest', 'user', 'virtual')
|
||||
unique_together = ("contest", "user", "virtual")
|
||||
|
||||
|
||||
class ContestProblem(models.Model):
|
||||
problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='contests', on_delete=CASCADE)
|
||||
contest = models.ForeignKey(Contest, verbose_name=_('contest'), related_name='contest_problems', on_delete=CASCADE)
|
||||
points = models.IntegerField(verbose_name=_('points'))
|
||||
partial = models.BooleanField(default=True, verbose_name=_('partial'))
|
||||
is_pretested = models.BooleanField(default=False, verbose_name=_('is pretested'))
|
||||
order = models.PositiveIntegerField(db_index=True, verbose_name=_('order'))
|
||||
output_prefix_override = models.IntegerField(help_text=_('0 to not show testcases, 1 to show'),
|
||||
verbose_name=_('visible testcases'), null=True, blank=True, default=0)
|
||||
max_submissions = models.IntegerField(help_text=_('Maximum number of submissions for this problem, '
|
||||
'or 0 for no limit.'), default=0,
|
||||
validators=[MinValueValidator(0, _('Why include a problem you '
|
||||
'can\'t submit to?'))])
|
||||
problem = models.ForeignKey(
|
||||
Problem, verbose_name=_("problem"), related_name="contests", on_delete=CASCADE
|
||||
)
|
||||
contest = models.ForeignKey(
|
||||
Contest,
|
||||
verbose_name=_("contest"),
|
||||
related_name="contest_problems",
|
||||
on_delete=CASCADE,
|
||||
)
|
||||
points = models.IntegerField(verbose_name=_("points"))
|
||||
partial = models.BooleanField(default=True, verbose_name=_("partial"))
|
||||
is_pretested = models.BooleanField(default=False, verbose_name=_("is pretested"))
|
||||
order = models.PositiveIntegerField(db_index=True, verbose_name=_("order"))
|
||||
output_prefix_override = models.IntegerField(
|
||||
help_text=_("0 to not show testcases, 1 to show"),
|
||||
verbose_name=_("visible testcases"),
|
||||
null=True,
|
||||
blank=True,
|
||||
default=0,
|
||||
)
|
||||
max_submissions = models.IntegerField(
|
||||
help_text=_(
|
||||
"Maximum number of submissions for this problem, " "or 0 for no limit."
|
||||
),
|
||||
default=0,
|
||||
validators=[
|
||||
MinValueValidator(0, _("Why include a problem you " "can't submit to?"))
|
||||
],
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('problem', 'contest')
|
||||
verbose_name = _('contest problem')
|
||||
verbose_name_plural = _('contest problems')
|
||||
unique_together = ("problem", "contest")
|
||||
verbose_name = _("contest problem")
|
||||
verbose_name_plural = _("contest problems")
|
||||
|
||||
|
||||
class ContestSubmission(models.Model):
|
||||
submission = models.OneToOneField(Submission, verbose_name=_('submission'),
|
||||
related_name='contest', on_delete=CASCADE)
|
||||
problem = models.ForeignKey(ContestProblem, verbose_name=_('problem'), on_delete=CASCADE,
|
||||
related_name='submissions', related_query_name='submission')
|
||||
participation = models.ForeignKey(ContestParticipation, verbose_name=_('participation'), on_delete=CASCADE,
|
||||
related_name='submissions', related_query_name='submission')
|
||||
points = models.FloatField(default=0.0, verbose_name=_('points'))
|
||||
is_pretest = models.BooleanField(verbose_name=_('is pretested'),
|
||||
help_text=_('Whether this submission was ran only on pretests.'),
|
||||
default=False)
|
||||
submission = models.OneToOneField(
|
||||
Submission,
|
||||
verbose_name=_("submission"),
|
||||
related_name="contest",
|
||||
on_delete=CASCADE,
|
||||
)
|
||||
problem = models.ForeignKey(
|
||||
ContestProblem,
|
||||
verbose_name=_("problem"),
|
||||
on_delete=CASCADE,
|
||||
related_name="submissions",
|
||||
related_query_name="submission",
|
||||
)
|
||||
participation = models.ForeignKey(
|
||||
ContestParticipation,
|
||||
verbose_name=_("participation"),
|
||||
on_delete=CASCADE,
|
||||
related_name="submissions",
|
||||
related_query_name="submission",
|
||||
)
|
||||
points = models.FloatField(default=0.0, verbose_name=_("points"))
|
||||
is_pretest = models.BooleanField(
|
||||
verbose_name=_("is pretested"),
|
||||
help_text=_("Whether this submission was ran only on pretests."),
|
||||
default=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('contest submission')
|
||||
verbose_name_plural = _('contest submissions')
|
||||
verbose_name = _("contest submission")
|
||||
verbose_name_plural = _("contest submissions")
|
||||
|
||||
|
||||
class Rating(models.Model):
|
||||
user = models.ForeignKey(Profile, verbose_name=_('user'), related_name='ratings', on_delete=CASCADE)
|
||||
contest = models.ForeignKey(Contest, verbose_name=_('contest'), related_name='ratings', on_delete=CASCADE)
|
||||
participation = models.OneToOneField(ContestParticipation, verbose_name=_('participation'),
|
||||
related_name='rating', on_delete=CASCADE)
|
||||
rank = models.IntegerField(verbose_name=_('rank'))
|
||||
rating = models.IntegerField(verbose_name=_('rating'))
|
||||
mean = models.FloatField(verbose_name=_('raw rating'))
|
||||
performance = models.FloatField(verbose_name=_('contest performance'))
|
||||
last_rated = models.DateTimeField(db_index=True, verbose_name=_('last rated'))
|
||||
user = models.ForeignKey(
|
||||
Profile, verbose_name=_("user"), related_name="ratings", on_delete=CASCADE
|
||||
)
|
||||
contest = models.ForeignKey(
|
||||
Contest, verbose_name=_("contest"), related_name="ratings", on_delete=CASCADE
|
||||
)
|
||||
participation = models.OneToOneField(
|
||||
ContestParticipation,
|
||||
verbose_name=_("participation"),
|
||||
related_name="rating",
|
||||
on_delete=CASCADE,
|
||||
)
|
||||
rank = models.IntegerField(verbose_name=_("rank"))
|
||||
rating = models.IntegerField(verbose_name=_("rating"))
|
||||
mean = models.FloatField(verbose_name=_("raw rating"))
|
||||
performance = models.FloatField(verbose_name=_("contest performance"))
|
||||
last_rated = models.DateTimeField(db_index=True, verbose_name=_("last rated"))
|
||||
|
||||
class Meta:
|
||||
unique_together = ('user', 'contest')
|
||||
verbose_name = _('contest rating')
|
||||
verbose_name_plural = _('contest ratings')
|
||||
unique_together = ("user", "contest")
|
||||
verbose_name = _("contest rating")
|
||||
verbose_name_plural = _("contest ratings")
|
||||
|
||||
|
||||
class ContestMoss(models.Model):
|
||||
LANG_MAPPING = [
|
||||
('C', MOSS_LANG_C),
|
||||
('C++', MOSS_LANG_CC),
|
||||
('Java', MOSS_LANG_JAVA),
|
||||
('Python', MOSS_LANG_PYTHON),
|
||||
('Pascal', MOSS_LANG_PASCAL),
|
||||
("C", MOSS_LANG_C),
|
||||
("C++", MOSS_LANG_CC),
|
||||
("Java", MOSS_LANG_JAVA),
|
||||
("Python", MOSS_LANG_PYTHON),
|
||||
("Pascal", MOSS_LANG_PASCAL),
|
||||
]
|
||||
|
||||
contest = models.ForeignKey(Contest, verbose_name=_('contest'), related_name='moss', on_delete=CASCADE)
|
||||
problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='moss', on_delete=CASCADE)
|
||||
contest = models.ForeignKey(
|
||||
Contest, verbose_name=_("contest"), related_name="moss", on_delete=CASCADE
|
||||
)
|
||||
problem = models.ForeignKey(
|
||||
Problem, verbose_name=_("problem"), related_name="moss", on_delete=CASCADE
|
||||
)
|
||||
language = models.CharField(max_length=10)
|
||||
submission_count = models.PositiveIntegerField(default=0)
|
||||
url = models.URLField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('contest', 'problem', 'language')
|
||||
verbose_name = _('contest moss result')
|
||||
verbose_name_plural = _('contest moss results')
|
||||
unique_together = ("contest", "problem", "language")
|
||||
verbose_name = _("contest moss result")
|
||||
verbose_name_plural = _("contest moss results")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue