2020-01-21 06:35:58 +00:00
|
|
|
from django.core.exceptions import ValidationError
|
2020-10-26 20:13:17 +00:00
|
|
|
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
|
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 _
|
2023-02-20 23:15:13 +00:00
|
|
|
from django.contrib.contenttypes.fields import GenericRelation
|
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,
|
|
|
|
)
|
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
|
2023-08-03 16:19:45 +00:00
|
|
|
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",
|
2022-10-13 02:19:22 +00:00
|
|
|
"ContestProblemClarification",
|
2023-10-06 08:54:37 +00:00
|
|
|
"ContestsSummary",
|
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
|
|
|
|
|
|
|
|
2023-08-03 16:19:45 +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,
|
2023-10-27 21:49:28 +00:00
|
|
|
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,
|
2023-10-27 21:49:28 +00:00
|
|
|
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,
|
2023-10-27 21:49:28 +00:00
|
|
|
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"),
|
|
|
|
)
|
|
|
|
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."
|
|
|
|
),
|
|
|
|
)
|
2023-02-20 23:15:13 +00:00
|
|
|
comments = GenericRelation("Comment")
|
2023-08-03 09:04:39 +00:00
|
|
|
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:
|
2022-05-14 17:57:27 +00:00
|
|
|
raise ValidationError(
|
|
|
|
"What is this? A contest that ended before it starts?"
|
|
|
|
)
|
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
|
|
|
|
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):
|
2023-11-29 02:04:02 +00:00
|
|
|
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()
|
|
|
|
|
|
|
|
# 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(
|
|
|
|
is_visible=True, is_organization_private=False, is_private=False
|
|
|
|
)
|
|
|
|
.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
|
|
|
):
|
2021-05-24 20:00:36 +00:00
|
|
|
q = Q(is_visible=True)
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
)
|
2022-12-28 20:38:32 +00:00
|
|
|
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"))
|
2023-08-25 23:12:53 +00:00
|
|
|
show_testcases = models.BooleanField(
|
2022-05-14 17:57:27 +00:00
|
|
|
verbose_name=_("visible testcases"),
|
2023-08-25 23:12:53 +00:00
|
|
|
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."
|
|
|
|
),
|
2022-09-15 07:05:02 +00:00
|
|
|
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
|
|
|
|
2022-10-13 02:19:22 +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")
|
2022-10-13 02:19:22 +00:00
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
)
|
2023-11-24 06:35:38 +00:00
|
|
|
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])
|