NDOJ/judge/models/contest.py

1062 lines
34 KiB
Python
Raw Permalink Normal View History

2020-01-21 06:35:58 +00:00
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
2020-01-21 06:35:58 +00:00
from django.db import models, transaction
2021-05-24 20:00:36 +00:00
from django.db.models import CASCADE, Q
from django.db.models.signals import m2m_changed
2020-01-21 06:35:58 +00:00
from django.urls import reverse
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import gettext, gettext_lazy as _
from django.contrib.contenttypes.fields import GenericRelation
from django.dispatch import receiver
2020-01-21 06:35:58 +00:00
from jsonfield import JSONField
2021-05-24 20:00:36 +00:00
from lupa import LuaRuntime
2022-05-14 17:57:27 +00:00
from moss import (
MOSS_LANG_C,
MOSS_LANG_CC,
MOSS_LANG_JAVA,
MOSS_LANG_PYTHON,
MOSS_LANG_PASCAL,
)
2024-09-03 15:48:01 +00:00
from datetime import timedelta, datetime
2020-01-21 06:35:58 +00:00
from judge import contest_format
from judge.models.problem import Problem
from judge.models.profile import Organization, Profile
from judge.models.submission import Submission
from judge.ratings import rate_contest
from judge.models.pagevote import PageVotable
from judge.models.bookmark import Bookmarkable
2023-10-14 19:56:22 +00:00
from judge.fulltext import SearchManager
2024-04-25 06:58:47 +00:00
from judge.caching import cache_wrapper
2020-01-21 06:35:58 +00:00
2022-05-14 17:57:27 +00:00
__all__ = [
"Contest",
"ContestTag",
"ContestParticipation",
"ContestProblem",
"ContestSubmission",
"Rating",
"ContestProblemClarification",
2023-10-06 08:54:37 +00:00
"ContestsSummary",
2024-05-30 07:59:22 +00:00
"OfficialContest",
"OfficialContestCategory",
"OfficialContestLocation",
2022-05-14 17:57:27 +00:00
]
2020-01-21 06:35:58 +00:00
class ContestTag(models.Model):
2022-05-14 17:57:27 +00:00
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)
2020-01-21 06:35:58 +00:00
def __str__(self):
return self.name
def get_absolute_url(self):
2022-05-14 17:57:27 +00:00
return reverse("contest_tag", args=[self.name])
2020-01-21 06:35:58 +00:00
@property
def text_color(self, cache={}):
if self.color not in cache:
if len(self.color) == 4:
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:])]
2022-05-14 17:57:27 +00:00
cache[self.color] = (
"#000" if 299 * r + 587 * g + 144 * b > 140000 else "#fff"
)
2020-01-21 06:35:58 +00:00
return cache[self.color]
class Meta:
2022-05-14 17:57:27 +00:00
verbose_name = _("contest tag")
verbose_name_plural = _("contest tags")
2020-01-21 06:35:58 +00:00
class Contest(models.Model, PageVotable, Bookmarkable):
2022-05-14 17:57:27 +00:00
SCOREBOARD_VISIBLE = "V"
SCOREBOARD_AFTER_CONTEST = "C"
SCOREBOARD_AFTER_PARTICIPATION = "P"
2021-05-24 20:00:36 +00:00
SCOREBOARD_VISIBILITY = (
2022-05-14 17:57:27 +00:00
(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,
verbose_name=_("authors"),
2022-05-14 17:57:27 +00:00
help_text=_("These users will be able to edit the contest."),
related_name="authors+",
)
curators = models.ManyToManyField(
Profile,
verbose_name=_("curators"),
2022-05-14 17:57:27 +00:00
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,
verbose_name=_("testers"),
2022-05-14 17:57:27 +00:00
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"
),
)
2022-11-18 22:59:58 +00:00
freeze_after = models.DurationField(
verbose_name=_("freeze after"),
blank=True,
null=True,
help_text=_(
"Format hh:mm:ss. For example, if you want to freeze contest after 2 hours, enter 02:00:00"
),
)
2022-05-14 17:57:27 +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."),
)
2023-09-17 04:55:24 +00:00
public_scoreboard = models.BooleanField(
verbose_name=_("public scoreboard"),
help_text=_("Ranking page is public even for private contests."),
default=False,
)
2022-05-14 17:57:27 +00:00
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"),
)
2024-10-02 20:06:33 +00:00
is_in_course = models.BooleanField(
verbose_name=_("contest in course"),
default=False,
)
2022-05-14 17:57:27 +00:00
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."),
)
2024-03-23 05:26:53 +00:00
rate_limit = models.PositiveIntegerField(
verbose_name=(_("rate limit")),
null=True,
blank=True,
validators=[MinValueValidator(1), MaxValueValidator(5)],
help_text=_(
"Maximum number of submissions per minute. Leave empty if you don't want rate limit."
),
)
comments = GenericRelation("Comment")
pagevote = GenericRelation("PageVote")
bookmark = GenericRelation("BookMark")
2023-10-14 19:56:22 +00:00
objects = SearchManager(("key", "name"))
2022-05-14 17:57:27 +00:00
2020-01-21 06:35:58 +00:00
@cached_property
def format_class(self):
return contest_format.formats[self.format_name]
@cached_property
def format(self):
return self.format_class(self, self.format_config)
2021-05-24 20:00:36 +00:00
@cached_property
def get_label_for_problem(self):
def DENY_ALL(obj, attr_name, is_setting):
raise AttributeError()
2022-05-14 17:57:27 +00:00
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()
)
2021-05-24 20:00:36 +00:00
2020-01-21 06:35:58 +00:00
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:
2024-08-14 09:15:19 +00:00
raise ValidationError(_("End time must be after start time"))
2020-01-21 06:35:58 +00:00
self.format_class.validate(self.format_config)
2021-05-24 20:00:36 +00:00
try:
# a contest should have at least one problem, with contest problem index 0
# so test it to see if the script returns a valid label.
label = self.get_label_for_problem(0)
except Exception as e:
2022-05-14 17:57:27 +00:00
raise ValidationError("Contest problem label script: %s" % e)
2021-05-24 20:00:36 +00:00
else:
if not isinstance(label, str):
2022-05-14 17:57:27 +00:00
raise ValidationError(
"Contest problem label script: script should return a string."
)
2021-05-24 20:00:36 +00:00
2024-08-14 09:15:19 +00:00
def save(self, *args, **kwargs):
2024-09-03 15:48:01 +00:00
earliest_start_time = datetime(2020, 1, 1).replace(tzinfo=timezone.utc)
if self.start_time < earliest_start_time:
self.start_time = earliest_start_time
if self.end_time < self.start_time:
self.end_time = self.start_time + timedelta(hours=1)
2024-08-14 09:15:19 +00:00
one_year_later = self.start_time + timedelta(days=365)
if self.end_time > one_year_later:
self.end_time = one_year_later
2024-08-14 12:34:29 +00:00
max_duration = timedelta(days=7)
if self.time_limit and self.time_limit > max_duration:
self.time_limit = max_duration
2024-08-14 09:15:19 +00:00
super().save(*args, **kwargs)
2020-01-21 06:35:58 +00:00
def is_in_contest(self, user):
if user.is_authenticated:
profile = user.profile
2022-05-14 17:57:27 +00:00
return (
profile
and profile.current_contest is not None
and profile.current_contest.contest == self
)
2020-01-21 06:35:58 +00:00
return False
2021-05-24 20:00:36 +00:00
def can_see_own_scoreboard(self, user):
if self.can_see_full_scoreboard(user):
2020-01-21 06:35:58 +00:00
return True
2021-05-24 20:00:36 +00:00
if not self.can_join:
return False
if not self.show_scoreboard and not self.is_in_contest(user):
return False
return True
def can_see_full_scoreboard(self, user):
if self.show_scoreboard:
2020-12-28 01:59:57 +00:00
return True
2021-05-24 20:00:36 +00:00
if not user.is_authenticated:
2020-01-21 06:35:58 +00:00
return False
2022-05-14 17:57:27 +00:00
if user.has_perm("judge.see_private_contest") or user.has_perm(
"judge.edit_all_contest"
):
2021-05-24 20:00:36 +00:00
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
2022-05-14 17:57:27 +00:00
if (
self.scoreboard_visibility == self.SCOREBOARD_AFTER_PARTICIPATION
and self.has_completed_contest(user)
):
2021-05-24 20:00:36 +00:00
return True
return False
def has_completed_contest(self, user):
if user.is_authenticated:
2022-05-14 17:57:27 +00:00
participation = self.users.filter(
virtual=ContestParticipation.LIVE, user=user.profile
).first()
2021-05-24 20:00:36 +00:00
if participation and participation.ended:
return True
return False
@cached_property
def show_scoreboard(self):
if not self.can_join:
2020-01-21 06:35:58 +00:00
return False
2022-05-14 17:57:27 +00:00
if (
self.scoreboard_visibility
in (self.SCOREBOARD_AFTER_CONTEST, self.SCOREBOARD_AFTER_PARTICIPATION)
and not self.ended
):
2020-01-21 06:35:58 +00:00
return False
return True
@property
def contest_window_length(self):
return self.end_time - self.start_time
@cached_property
def _now(self):
# This ensures that all methods talk about the same now.
return timezone.now()
@cached_property
def can_join(self):
return self.start_time <= self._now
@property
def time_before_start(self):
if self.start_time >= self._now:
return self.start_time - self._now
else:
return None
@property
def time_before_end(self):
if self.end_time >= self._now:
return self.end_time - self._now
else:
return None
@cached_property
def ended(self):
return self.end_time < self._now
2024-04-25 06:58:47 +00:00
@cache_wrapper(prefix="Coai")
def _author_ids(self):
return set(
Contest.authors.through.objects.filter(contest=self).values_list(
"profile_id", flat=True
)
2022-05-14 17:57:27 +00:00
)
2021-05-24 20:00:36 +00:00
2024-04-25 06:58:47 +00:00
@cache_wrapper(prefix="Coci")
def _curator_ids(self):
return set(
2022-05-14 17:57:27 +00:00
Contest.curators.through.objects.filter(contest=self).values_list(
"profile_id", flat=True
)
)
2021-05-24 20:00:36 +00:00
2024-04-25 06:58:47 +00:00
@cache_wrapper(prefix="Coti")
def _tester_ids(self):
return set(
Contest.testers.through.objects.filter(contest=self).values_list(
"profile_id", flat=True
)
)
@cached_property
def author_ids(self):
return self._author_ids()
@cached_property
def editor_ids(self):
return self.author_ids.union(self._curator_ids())
2021-05-24 20:00:36 +00:00
@cached_property
def tester_ids(self):
2024-04-25 06:58:47 +00:00
return self._tester_ids()
2021-05-24 20:00:36 +00:00
2020-01-21 06:35:58 +00:00
def __str__(self):
return f"{self.name} ({self.key})"
2020-01-21 06:35:58 +00:00
def get_absolute_url(self):
2022-05-14 17:57:27 +00:00
return reverse("contest_view", args=(self.key,))
2020-01-21 06:35:58 +00:00
def update_user_count(self):
self.user_count = self.users.filter(virtual=0).count()
self.save()
update_user_count.alters_data = True
2021-05-24 20:00:36 +00:00
class Inaccessible(Exception):
pass
class PrivateContest(Exception):
pass
def access_check(self, user):
# Do unauthenticated check here so we can skip authentication checks later on.
if not user.is_authenticated:
# Unauthenticated users can only see visible, non-private contests
if not self.is_visible:
raise self.Inaccessible()
if self.is_private or self.is_organization_private:
raise self.PrivateContest()
return
# If the user can view or edit all contests
2022-05-14 17:57:27 +00:00
if user.has_perm("judge.see_private_contest") or user.has_perm(
"judge.edit_all_contest"
):
2021-05-24 20:00:36 +00:00
return
# User is organizer or curator for contest
if user.profile.id in self.editor_ids:
return
# User is tester for contest
if user.profile.id in self.tester_ids:
return
# Contest is not publicly visible
if not self.is_visible:
raise self.Inaccessible()
2024-10-02 20:06:33 +00:00
if self.is_in_course:
from judge.models import Course, CourseContest
course_contest = CourseContest.objects.filter(contest=self).first()
if Course.is_accessible_by(course_contest.course, user.profile):
return
raise self.Inaccessible()
2021-05-24 20:00:36 +00:00
# Contest is not private
if not self.is_private and not self.is_organization_private:
return
if self.view_contest_scoreboard.filter(id=user.profile.id).exists():
return
2022-05-14 17:57:27 +00:00
in_org = self.organizations.filter(
id__in=user.profile.organizations.all()
).exists()
2021-05-24 20:00:36 +00:00
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
2022-05-14 17:57:27 +00:00
raise self.PrivateContest()
2021-05-24 20:00:36 +00:00
if self.is_private and not self.is_organization_private:
if in_users:
return
raise self.PrivateContest()
if self.is_private and self.is_organization_private:
if in_org and in_users:
return
raise self.PrivateContest()
2020-01-21 06:35:58 +00:00
def is_accessible_by(self, user):
2021-05-24 20:00:36 +00:00
try:
self.access_check(user)
except (self.Inaccessible, self.PrivateContest):
return False
else:
2020-01-21 06:35:58 +00:00
return True
def is_editable_by(self, user):
# If the user can edit all contests
2022-05-14 17:57:27 +00:00
if user.has_perm("judge.edit_all_contest"):
2020-01-21 06:35:58 +00:00
return True
2021-05-24 20:00:36 +00:00
# If the user is a contest organizer or curator
2023-08-07 13:36:28 +00:00
if hasattr(user, "profile") and user.profile.id in self.editor_ids:
2020-01-21 06:35:58 +00:00
return True
return False
2021-05-24 20:00:36 +00:00
@classmethod
2022-10-10 07:07:50 +00:00
def get_visible_contests(cls, user, show_own_contests_only=False):
2022-12-28 20:59:15 +00:00
if not user.is_authenticated:
2022-05-14 17:57:27 +00:00
return (
cls.objects.filter(
2024-10-02 20:06:33 +00:00
is_visible=True,
is_organization_private=False,
is_private=False,
is_in_course=False,
2022-05-14 17:57:27 +00:00
)
.defer("description")
.distinct()
)
2021-05-24 20:00:36 +00:00
2022-05-14 17:57:27 +00:00
queryset = cls.objects.defer("description")
2022-12-28 20:59:15 +00:00
if (
not (
user.has_perm("judge.see_private_contest")
or user.has_perm("judge.edit_all_contest")
)
or show_own_contests_only
2022-05-14 17:57:27 +00:00
):
2024-10-02 20:06:33 +00:00
q = Q(is_visible=True, is_in_course=False)
2021-05-24 20:00:36 +00:00
q &= (
2022-05-14 17:57:27 +00:00
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,
)
2021-05-24 20:00:36 +00:00
)
q |= Q(authors=user.profile)
q |= Q(curators=user.profile)
q |= Q(testers=user.profile)
queryset = queryset.filter(q)
return queryset.distinct()
2020-01-21 06:35:58 +00:00
def rate(self):
2022-05-14 17:57:27 +00:00
Rating.objects.filter(
contest__end_time__range=(self.end_time, self._now)
).delete()
2021-05-24 20:00:36 +00:00
for contest in Contest.objects.filter(
2022-05-14 17:57:27 +00:00
is_rated=True,
end_time__range=(self.end_time, self._now),
).order_by("end_time"):
2020-01-21 06:35:58 +00:00
rate_contest(contest)
class Meta:
permissions = (
2022-05-14 17:57:27 +00:00
("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")),
2020-01-21 06:35:58 +00:00
)
2022-05-14 17:57:27 +00:00
verbose_name = _("contest")
verbose_name_plural = _("contests")
2020-01-21 06:35:58 +00:00
@receiver(m2m_changed, sender=Contest.organizations.through)
def update_organization_private(sender, instance, **kwargs):
if kwargs["action"] in ["post_add", "post_remove", "post_clear"]:
instance.is_organization_private = instance.organizations.exists()
instance.save(update_fields=["is_organization_private"])
@receiver(m2m_changed, sender=Contest.private_contestants.through)
def update_private(sender, instance, **kwargs):
if kwargs["action"] in ["post_add", "post_remove", "post_clear"]:
instance.is_private = instance.private_contestants.exists()
instance.save(update_fields=["is_private"])
2020-01-21 06:35:58 +00:00
class ContestParticipation(models.Model):
LIVE = 0
SPECTATE = -1
2022-05-14 17:57:27 +00:00
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
)
format_data_final = JSONField(
verbose_name=_("same as format_data, but including frozen results"),
null=True,
blank=True,
)
score_final = models.FloatField(verbose_name=_("final score"), default=0)
cumtime_final = models.PositiveIntegerField(
verbose_name=_("final cumulative time"), default=0
)
2020-01-21 06:35:58 +00:00
def recompute_results(self):
with transaction.atomic():
self.contest.format.update_participation(self)
if self.is_disqualified:
self.score = -9999
2022-05-14 17:57:27 +00:00
self.save(update_fields=["score"])
2020-01-21 06:35:58 +00:00
recompute_results.alters_data = True
def set_disqualified(self, disqualified):
self.is_disqualified = disqualified
self.recompute_results()
if self.contest.is_rated and self.contest.ratings.exists():
self.contest.rate()
if self.is_disqualified:
if self.user.current_contest == self:
self.user.remove_contest()
self.contest.banned_users.add(self.user)
else:
self.contest.banned_users.remove(self.user)
2022-05-14 17:57:27 +00:00
2020-01-21 06:35:58 +00:00
set_disqualified.alters_data = True
@property
def live(self):
return self.virtual == self.LIVE
@property
def spectate(self):
return self.virtual == self.SPECTATE
@cached_property
def start(self):
contest = self.contest
2022-05-14 17:57:27 +00:00
return (
contest.start_time
if contest.time_limit is None and (self.live or self.spectate)
else self.real_start
)
2020-01-21 06:35:58 +00:00
@cached_property
def end_time(self):
contest = self.contest
if self.spectate:
return contest.end_time
if self.virtual:
if contest.time_limit:
return self.real_start + contest.time_limit
else:
return self.real_start + (contest.end_time - contest.start_time)
2022-05-14 17:57:27 +00:00
return (
contest.end_time
if contest.time_limit is None
else min(self.real_start + contest.time_limit, contest.end_time)
)
2020-01-21 06:35:58 +00:00
@cached_property
def _now(self):
# This ensures that all methods talk about the same now.
return timezone.now()
@property
def ended(self):
return self.end_time is not None and self.end_time < self._now
@property
def time_remaining(self):
end = self.end_time
if end is not None and end >= self._now:
return end - self._now
def __str__(self):
if self.spectate:
2022-05-14 17:57:27 +00:00
return gettext("%s spectating in %s") % (
self.user.username,
self.contest.name,
)
2020-01-21 06:35:58 +00:00
if self.virtual:
2022-05-14 17:57:27 +00:00
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)
2020-01-21 06:35:58 +00:00
class Meta:
2022-05-14 17:57:27 +00:00
verbose_name = _("contest participation")
verbose_name_plural = _("contest participations")
2020-01-21 06:35:58 +00:00
2022-05-14 17:57:27 +00:00
unique_together = ("contest", "user", "virtual")
2020-01-21 06:35:58 +00:00
class ContestProblem(models.Model):
2022-05-14 17:57:27 +00:00
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"))
show_testcases = models.BooleanField(
2022-05-14 17:57:27 +00:00
verbose_name=_("visible testcases"),
default=False,
2022-05-14 17:57:27 +00:00
)
max_submissions = models.IntegerField(
help_text=_(
"Maximum number of submissions for this problem, " "or 0 for no limit."
),
verbose_name=_("max submissions"),
2022-05-14 17:57:27 +00:00
default=0,
validators=[
MinValueValidator(0, _("Why include a problem you " "can't submit to?"))
],
)
2023-01-02 23:22:45 +00:00
hidden_subtasks = models.CharField(
help_text=_("Separated by commas, e.g: 2, 3"),
verbose_name=_("hidden subtasks"),
2022-12-20 08:24:24 +00:00
null=True,
blank=True,
max_length=20,
)
2020-01-21 06:35:58 +00:00
@property
def clarifications(self):
return ContestProblemClarification.objects.filter(problem=self)
2020-01-21 06:35:58 +00:00
class Meta:
2022-05-14 17:57:27 +00:00
unique_together = ("problem", "contest")
verbose_name = _("contest problem")
verbose_name_plural = _("contest problems")
2020-01-21 06:35:58 +00:00
class ContestSubmission(models.Model):
2022-05-14 17:57:27 +00:00
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,
)
2020-01-21 06:35:58 +00:00
class Meta:
2022-05-14 17:57:27 +00:00
verbose_name = _("contest submission")
verbose_name_plural = _("contest submissions")
2020-01-21 06:35:58 +00:00
class Rating(models.Model):
2022-05-14 17:57:27 +00:00
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"))
2020-01-21 06:35:58 +00:00
class Meta:
2022-05-14 17:57:27 +00:00
unique_together = ("user", "contest")
verbose_name = _("contest rating")
verbose_name_plural = _("contest ratings")
2020-01-21 06:35:58 +00:00
class ContestMoss(models.Model):
LANG_MAPPING = [
2022-05-14 17:57:27 +00:00
("C", MOSS_LANG_C),
("C++", MOSS_LANG_CC),
("Java", MOSS_LANG_JAVA),
("Python", MOSS_LANG_PYTHON),
("Pascal", MOSS_LANG_PASCAL),
2020-01-21 06:35:58 +00:00
]
2022-05-14 17:57:27 +00:00
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
)
2020-01-21 06:35:58 +00:00
language = models.CharField(max_length=10)
submission_count = models.PositiveIntegerField(default=0)
url = models.URLField(null=True, blank=True)
class Meta:
2022-05-14 17:57:27 +00:00
unique_together = ("contest", "problem", "language")
verbose_name = _("contest moss result")
verbose_name_plural = _("contest moss results")
class ContestProblemClarification(models.Model):
problem = models.ForeignKey(
ContestProblem, verbose_name=_("clarified problem"), on_delete=CASCADE
)
description = models.TextField(verbose_name=_("clarification body"))
date = models.DateTimeField(
verbose_name=_("clarification timestamp"), auto_now_add=True
)
2023-10-06 08:54:37 +00:00
class ContestsSummary(models.Model):
contests = models.ManyToManyField(
Contest,
)
scores = models.JSONField(
null=True,
blank=True,
)
key = models.CharField(
max_length=20,
unique=True,
)
results = models.JSONField(null=True, blank=True)
2023-10-06 08:54:37 +00:00
class Meta:
verbose_name = _("contests summary")
verbose_name_plural = _("contests summaries")
def __str__(self):
return self.key
2023-10-06 18:04:12 +00:00
def get_absolute_url(self):
return reverse("contests_summary", args=[self.key])
2024-05-30 07:59:22 +00:00
class OfficialContestCategory(models.Model):
name = models.CharField(
max_length=50, verbose_name=_("official contest category"), unique=True
)
def __str__(self):
return self.name
class Meta:
verbose_name = _("official contest category")
verbose_name_plural = _("official contest categories")
class OfficialContestLocation(models.Model):
name = models.CharField(
max_length=50, verbose_name=_("official contest location"), unique=True
)
def __str__(self):
return self.name
class Meta:
verbose_name = _("official contest location")
verbose_name_plural = _("official contest locations")
class OfficialContest(models.Model):
contest = models.OneToOneField(
Contest,
verbose_name=_("contest"),
related_name="official",
on_delete=CASCADE,
)
category = models.ForeignKey(
OfficialContestCategory,
verbose_name=_("contest category"),
on_delete=CASCADE,
)
year = models.PositiveIntegerField(verbose_name=_("year"))
location = models.ForeignKey(
OfficialContestLocation,
verbose_name=_("contest location"),
on_delete=CASCADE,
)
class Meta:
verbose_name = _("official contest")
verbose_name_plural = _("official contests")