Reformat using black
This commit is contained in:
parent
efee4ad081
commit
a87fb49918
221 changed files with 19127 additions and 7310 deletions
|
@ -1,32 +1,67 @@
|
|||
from reversion import revisions
|
||||
|
||||
from judge.models.choices import ACE_THEMES, EFFECTIVE_MATH_ENGINES, MATH_ENGINES_CHOICES, TIMEZONE
|
||||
from judge.models.choices import (
|
||||
ACE_THEMES,
|
||||
EFFECTIVE_MATH_ENGINES,
|
||||
MATH_ENGINES_CHOICES,
|
||||
TIMEZONE,
|
||||
)
|
||||
from judge.models.comment import Comment, CommentLock, CommentVote, Notification
|
||||
from judge.models.contest import Contest, ContestMoss, ContestParticipation, ContestProblem, ContestSubmission, \
|
||||
ContestTag, Rating
|
||||
from judge.models.contest import (
|
||||
Contest,
|
||||
ContestMoss,
|
||||
ContestParticipation,
|
||||
ContestProblem,
|
||||
ContestSubmission,
|
||||
ContestTag,
|
||||
Rating,
|
||||
)
|
||||
from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex
|
||||
from judge.models.message import PrivateMessage, PrivateMessageThread
|
||||
from judge.models.problem import LanguageLimit, License, Problem, ProblemClarification, ProblemGroup, \
|
||||
ProblemTranslation, ProblemType, Solution, TranslatedProblemForeignKeyQuerySet, TranslatedProblemQuerySet, ProblemPointsVote
|
||||
from judge.models.problem_data import CHECKERS, ProblemData, ProblemTestCase, problem_data_storage, \
|
||||
problem_directory_file
|
||||
from judge.models.problem import (
|
||||
LanguageLimit,
|
||||
License,
|
||||
Problem,
|
||||
ProblemClarification,
|
||||
ProblemGroup,
|
||||
ProblemTranslation,
|
||||
ProblemType,
|
||||
Solution,
|
||||
TranslatedProblemForeignKeyQuerySet,
|
||||
TranslatedProblemQuerySet,
|
||||
ProblemPointsVote,
|
||||
)
|
||||
from judge.models.problem_data import (
|
||||
CHECKERS,
|
||||
ProblemData,
|
||||
ProblemTestCase,
|
||||
problem_data_storage,
|
||||
problem_directory_file,
|
||||
)
|
||||
from judge.models.profile import Organization, OrganizationRequest, Profile, Friend
|
||||
from judge.models.runtime import Judge, Language, RuntimeVersion
|
||||
from judge.models.submission import SUBMISSION_RESULT, Submission, SubmissionSource, SubmissionTestCase
|
||||
from judge.models.submission import (
|
||||
SUBMISSION_RESULT,
|
||||
Submission,
|
||||
SubmissionSource,
|
||||
SubmissionTestCase,
|
||||
)
|
||||
from judge.models.ticket import Ticket, TicketMessage
|
||||
from judge.models.volunteer import VolunteerProblemVote
|
||||
|
||||
revisions.register(Profile, exclude=['points', 'last_access', 'ip', 'rating'])
|
||||
revisions.register(Problem, follow=['language_limits'])
|
||||
revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"])
|
||||
revisions.register(Problem, follow=["language_limits"])
|
||||
revisions.register(LanguageLimit)
|
||||
revisions.register(Contest, follow=['contest_problems'])
|
||||
revisions.register(Contest, follow=["contest_problems"])
|
||||
revisions.register(ContestProblem)
|
||||
revisions.register(Organization)
|
||||
revisions.register(BlogPost)
|
||||
revisions.register(Solution)
|
||||
revisions.register(Judge, fields=['name', 'created', 'auth_key', 'description'])
|
||||
revisions.register(Judge, fields=["name", "created", "auth_key", "description"])
|
||||
revisions.register(Language)
|
||||
revisions.register(Comment, fields=['author', 'time', 'page', 'score', 'body', 'hidden', 'parent'])
|
||||
revisions.register(
|
||||
Comment, fields=["author", "time", "page", "score", "body", "hidden", "parent"]
|
||||
)
|
||||
revisions.register(ProblemTranslation)
|
||||
revisions.register(ProblemPointsVote)
|
||||
revisions.register(ContestMoss)
|
||||
|
|
|
@ -8,11 +8,11 @@ from django.utils.translation import gettext_lazy as _
|
|||
def make_timezones():
|
||||
data = defaultdict(list)
|
||||
for tz in pytz.all_timezones:
|
||||
if '/' in tz:
|
||||
area, loc = tz.split('/', 1)
|
||||
if "/" in tz:
|
||||
area, loc = tz.split("/", 1)
|
||||
else:
|
||||
area, loc = 'Other', tz
|
||||
if not loc.startswith('GMT'):
|
||||
area, loc = "Other", tz
|
||||
if not loc.startswith("GMT"):
|
||||
data[area].append((tz, loc))
|
||||
return sorted(data.items(), key=itemgetter(0))
|
||||
|
||||
|
@ -21,46 +21,46 @@ TIMEZONE = make_timezones()
|
|||
del make_timezones
|
||||
|
||||
ACE_THEMES = (
|
||||
('ambiance', 'Ambiance'),
|
||||
('chaos', 'Chaos'),
|
||||
('chrome', 'Chrome'),
|
||||
('clouds', 'Clouds'),
|
||||
('clouds_midnight', 'Clouds Midnight'),
|
||||
('cobalt', 'Cobalt'),
|
||||
('crimson_editor', 'Crimson Editor'),
|
||||
('dawn', 'Dawn'),
|
||||
('dreamweaver', 'Dreamweaver'),
|
||||
('eclipse', 'Eclipse'),
|
||||
('github', 'Github'),
|
||||
('idle_fingers', 'Idle Fingers'),
|
||||
('katzenmilch', 'Katzenmilch'),
|
||||
('kr_theme', 'KR Theme'),
|
||||
('kuroir', 'Kuroir'),
|
||||
('merbivore', 'Merbivore'),
|
||||
('merbivore_soft', 'Merbivore Soft'),
|
||||
('mono_industrial', 'Mono Industrial'),
|
||||
('monokai', 'Monokai'),
|
||||
('pastel_on_dark', 'Pastel on Dark'),
|
||||
('solarized_dark', 'Solarized Dark'),
|
||||
('solarized_light', 'Solarized Light'),
|
||||
('terminal', 'Terminal'),
|
||||
('textmate', 'Textmate'),
|
||||
('tomorrow', 'Tomorrow'),
|
||||
('tomorrow_night', 'Tomorrow Night'),
|
||||
('tomorrow_night_blue', 'Tomorrow Night Blue'),
|
||||
('tomorrow_night_bright', 'Tomorrow Night Bright'),
|
||||
('tomorrow_night_eighties', 'Tomorrow Night Eighties'),
|
||||
('twilight', 'Twilight'),
|
||||
('vibrant_ink', 'Vibrant Ink'),
|
||||
('xcode', 'XCode'),
|
||||
("ambiance", "Ambiance"),
|
||||
("chaos", "Chaos"),
|
||||
("chrome", "Chrome"),
|
||||
("clouds", "Clouds"),
|
||||
("clouds_midnight", "Clouds Midnight"),
|
||||
("cobalt", "Cobalt"),
|
||||
("crimson_editor", "Crimson Editor"),
|
||||
("dawn", "Dawn"),
|
||||
("dreamweaver", "Dreamweaver"),
|
||||
("eclipse", "Eclipse"),
|
||||
("github", "Github"),
|
||||
("idle_fingers", "Idle Fingers"),
|
||||
("katzenmilch", "Katzenmilch"),
|
||||
("kr_theme", "KR Theme"),
|
||||
("kuroir", "Kuroir"),
|
||||
("merbivore", "Merbivore"),
|
||||
("merbivore_soft", "Merbivore Soft"),
|
||||
("mono_industrial", "Mono Industrial"),
|
||||
("monokai", "Monokai"),
|
||||
("pastel_on_dark", "Pastel on Dark"),
|
||||
("solarized_dark", "Solarized Dark"),
|
||||
("solarized_light", "Solarized Light"),
|
||||
("terminal", "Terminal"),
|
||||
("textmate", "Textmate"),
|
||||
("tomorrow", "Tomorrow"),
|
||||
("tomorrow_night", "Tomorrow Night"),
|
||||
("tomorrow_night_blue", "Tomorrow Night Blue"),
|
||||
("tomorrow_night_bright", "Tomorrow Night Bright"),
|
||||
("tomorrow_night_eighties", "Tomorrow Night Eighties"),
|
||||
("twilight", "Twilight"),
|
||||
("vibrant_ink", "Vibrant Ink"),
|
||||
("xcode", "XCode"),
|
||||
)
|
||||
|
||||
MATH_ENGINES_CHOICES = (
|
||||
('tex', _('Leave as LaTeX')),
|
||||
('svg', _('SVG with PNG fallback')),
|
||||
('mml', _('MathML only')),
|
||||
('jax', _('MathJax with SVG/PNG fallback')),
|
||||
('auto', _('Detect best quality')),
|
||||
("tex", _("Leave as LaTeX")),
|
||||
("svg", _("SVG with PNG fallback")),
|
||||
("mml", _("MathML only")),
|
||||
("jax", _("MathJax with SVG/PNG fallback")),
|
||||
("auto", _("Detect best quality")),
|
||||
)
|
||||
|
||||
EFFECTIVE_MATH_ENGINES = ('svg', 'mml', 'tex', 'jax')
|
||||
EFFECTIVE_MATH_ENGINES = ("svg", "mml", "tex", "jax")
|
||||
|
|
|
@ -20,77 +20,98 @@ from judge.models.profile import Profile
|
|||
from judge.utils.cachedict import CacheDict
|
||||
|
||||
|
||||
__all__ = ['Comment', 'CommentLock', 'CommentVote', 'Notification']
|
||||
__all__ = ["Comment", "CommentLock", "CommentVote", "Notification"]
|
||||
|
||||
comment_validator = RegexValidator(r'^[pcs]:[a-z0-9]+$|^b:\d+$',
|
||||
_(r'Page code must be ^[pcs]:[a-z0-9]+$|^b:\d+$'))
|
||||
comment_validator = RegexValidator(
|
||||
r"^[pcs]:[a-z0-9]+$|^b:\d+$", _(r"Page code must be ^[pcs]:[a-z0-9]+$|^b:\d+$")
|
||||
)
|
||||
|
||||
|
||||
class VersionRelation(GenericRelation):
|
||||
def __init__(self):
|
||||
super(VersionRelation, self).__init__(Version, object_id_field='object_id')
|
||||
super(VersionRelation, self).__init__(Version, object_id_field="object_id")
|
||||
|
||||
def get_extra_restriction(self, where_class, alias, remote_alias):
|
||||
cond = super(VersionRelation, self).get_extra_restriction(where_class, alias, remote_alias)
|
||||
field = self.remote_field.model._meta.get_field('db')
|
||||
lookup = field.get_lookup('exact')(field.get_col(remote_alias), 'default')
|
||||
cond.add(lookup, 'AND')
|
||||
cond = super(VersionRelation, self).get_extra_restriction(
|
||||
where_class, alias, remote_alias
|
||||
)
|
||||
field = self.remote_field.model._meta.get_field("db")
|
||||
lookup = field.get_lookup("exact")(field.get_col(remote_alias), "default")
|
||||
cond.add(lookup, "AND")
|
||||
return cond
|
||||
|
||||
|
||||
class Comment(MPTTModel):
|
||||
author = models.ForeignKey(Profile, verbose_name=_('commenter'), on_delete=CASCADE)
|
||||
time = models.DateTimeField(verbose_name=_('posted time'), auto_now_add=True)
|
||||
page = models.CharField(max_length=30, verbose_name=_('associated page'), db_index=True,
|
||||
validators=[comment_validator])
|
||||
score = models.IntegerField(verbose_name=_('votes'), default=0)
|
||||
body = models.TextField(verbose_name=_('body of comment'), max_length=8192)
|
||||
hidden = models.BooleanField(verbose_name=_('hide the comment'), default=0)
|
||||
parent = TreeForeignKey('self', verbose_name=_('parent'), null=True, blank=True, related_name='replies',
|
||||
on_delete=CASCADE)
|
||||
author = models.ForeignKey(Profile, verbose_name=_("commenter"), on_delete=CASCADE)
|
||||
time = models.DateTimeField(verbose_name=_("posted time"), auto_now_add=True)
|
||||
page = models.CharField(
|
||||
max_length=30,
|
||||
verbose_name=_("associated page"),
|
||||
db_index=True,
|
||||
validators=[comment_validator],
|
||||
)
|
||||
score = models.IntegerField(verbose_name=_("votes"), default=0)
|
||||
body = models.TextField(verbose_name=_("body of comment"), max_length=8192)
|
||||
hidden = models.BooleanField(verbose_name=_("hide the comment"), default=0)
|
||||
parent = TreeForeignKey(
|
||||
"self",
|
||||
verbose_name=_("parent"),
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="replies",
|
||||
on_delete=CASCADE,
|
||||
)
|
||||
versions = VersionRelation()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('comment')
|
||||
verbose_name_plural = _('comments')
|
||||
verbose_name = _("comment")
|
||||
verbose_name_plural = _("comments")
|
||||
|
||||
class MPTTMeta:
|
||||
order_insertion_by = ['-time']
|
||||
order_insertion_by = ["-time"]
|
||||
|
||||
@classmethod
|
||||
def most_recent(cls, user, n, batch=None):
|
||||
queryset = cls.objects.filter(hidden=False).select_related('author__user') \
|
||||
.defer('author__about', 'body').order_by('-id')
|
||||
queryset = (
|
||||
cls.objects.filter(hidden=False)
|
||||
.select_related("author__user")
|
||||
.defer("author__about", "body")
|
||||
.order_by("-id")
|
||||
)
|
||||
|
||||
problem_access = CacheDict(lambda code: Problem.objects.get(code=code).is_accessible_by(user))
|
||||
contest_access = CacheDict(lambda key: Contest.objects.get(key=key).is_accessible_by(user))
|
||||
problem_access = CacheDict(
|
||||
lambda code: Problem.objects.get(code=code).is_accessible_by(user)
|
||||
)
|
||||
contest_access = CacheDict(
|
||||
lambda key: Contest.objects.get(key=key).is_accessible_by(user)
|
||||
)
|
||||
blog_access = CacheDict(lambda id: BlogPost.objects.get(id=id).can_see(user))
|
||||
|
||||
|
||||
if n == -1:
|
||||
n = len(queryset)
|
||||
n = len(queryset)
|
||||
if user.is_superuser:
|
||||
return queryset[:n]
|
||||
if batch is None:
|
||||
batch = 2 * n
|
||||
output = []
|
||||
for i in itertools.count(0):
|
||||
slice = queryset[i * batch:i * batch + batch]
|
||||
slice = queryset[i * batch : i * batch + batch]
|
||||
if not slice:
|
||||
break
|
||||
for comment in slice:
|
||||
if comment.page.startswith('p:') or comment.page.startswith('s:'):
|
||||
if comment.page.startswith("p:") or comment.page.startswith("s:"):
|
||||
try:
|
||||
if problem_access[comment.page[2:]]:
|
||||
output.append(comment)
|
||||
except Problem.DoesNotExist:
|
||||
pass
|
||||
elif comment.page.startswith('c:'):
|
||||
elif comment.page.startswith("c:"):
|
||||
try:
|
||||
if contest_access[comment.page[2:]]:
|
||||
output.append(comment)
|
||||
except Contest.DoesNotExist:
|
||||
pass
|
||||
elif comment.page.startswith('b:'):
|
||||
elif comment.page.startswith("b:"):
|
||||
try:
|
||||
if blog_access[comment.page[2:]]:
|
||||
output.append(comment)
|
||||
|
@ -106,50 +127,55 @@ class Comment(MPTTModel):
|
|||
def link(self):
|
||||
try:
|
||||
link = None
|
||||
if self.page.startswith('p:'):
|
||||
link = reverse('problem_detail', args=(self.page[2:],))
|
||||
elif self.page.startswith('c:'):
|
||||
link = reverse('contest_view', args=(self.page[2:],))
|
||||
elif self.page.startswith('b:'):
|
||||
key = 'blog_slug:%s' % self.page[2:]
|
||||
if self.page.startswith("p:"):
|
||||
link = reverse("problem_detail", args=(self.page[2:],))
|
||||
elif self.page.startswith("c:"):
|
||||
link = reverse("contest_view", args=(self.page[2:],))
|
||||
elif self.page.startswith("b:"):
|
||||
key = "blog_slug:%s" % self.page[2:]
|
||||
slug = cache.get(key)
|
||||
if slug is None:
|
||||
try:
|
||||
slug = BlogPost.objects.get(id=self.page[2:]).slug
|
||||
except ObjectDoesNotExist:
|
||||
slug = ''
|
||||
slug = ""
|
||||
cache.set(key, slug, 3600)
|
||||
link = reverse('blog_post', args=(self.page[2:], slug))
|
||||
elif self.page.startswith('s:'):
|
||||
link = reverse('problem_editorial', args=(self.page[2:],))
|
||||
link = reverse("blog_post", args=(self.page[2:], slug))
|
||||
elif self.page.startswith("s:"):
|
||||
link = reverse("problem_editorial", args=(self.page[2:],))
|
||||
except Exception:
|
||||
link = 'invalid'
|
||||
link = "invalid"
|
||||
return link
|
||||
|
||||
@classmethod
|
||||
def get_page_title(cls, page):
|
||||
try:
|
||||
if page.startswith('p:'):
|
||||
return Problem.objects.values_list('name', flat=True).get(code=page[2:])
|
||||
elif page.startswith('c:'):
|
||||
return Contest.objects.values_list('name', flat=True).get(key=page[2:])
|
||||
elif page.startswith('b:'):
|
||||
return BlogPost.objects.values_list('title', flat=True).get(id=page[2:])
|
||||
elif page.startswith('s:'):
|
||||
return _('Editorial for %s') % Problem.objects.values_list('name', flat=True).get(code=page[2:])
|
||||
return '<unknown>'
|
||||
if page.startswith("p:"):
|
||||
return Problem.objects.values_list("name", flat=True).get(code=page[2:])
|
||||
elif page.startswith("c:"):
|
||||
return Contest.objects.values_list("name", flat=True).get(key=page[2:])
|
||||
elif page.startswith("b:"):
|
||||
return BlogPost.objects.values_list("title", flat=True).get(id=page[2:])
|
||||
elif page.startswith("s:"):
|
||||
return _("Editorial for %s") % Problem.objects.values_list(
|
||||
"name", flat=True
|
||||
).get(code=page[2:])
|
||||
return "<unknown>"
|
||||
except ObjectDoesNotExist:
|
||||
return '<deleted>'
|
||||
return "<deleted>"
|
||||
|
||||
@cached_property
|
||||
def page_title(self):
|
||||
return self.get_page_title(self.page)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return '%s#comment-%d' % (self.link, self.id)
|
||||
return "%s#comment-%d" % (self.link, self.id)
|
||||
|
||||
def __str__(self):
|
||||
return '%(page)s by %(user)s' % {'page': self.page, 'user': self.author.user.username}
|
||||
return "%(page)s by %(user)s" % {
|
||||
"page": self.page,
|
||||
"user": self.author.user.username,
|
||||
}
|
||||
|
||||
# Only use this when queried with
|
||||
# .prefetch_related(Prefetch('votes', queryset=CommentVote.objects.filter(voter_id=profile_id)))
|
||||
|
@ -165,34 +191,52 @@ class Comment(MPTTModel):
|
|||
|
||||
|
||||
class CommentVote(models.Model):
|
||||
voter = models.ForeignKey(Profile, related_name='voted_comments', on_delete=CASCADE)
|
||||
comment = models.ForeignKey(Comment, related_name='votes', on_delete=CASCADE)
|
||||
voter = models.ForeignKey(Profile, related_name="voted_comments", on_delete=CASCADE)
|
||||
comment = models.ForeignKey(Comment, related_name="votes", on_delete=CASCADE)
|
||||
score = models.IntegerField()
|
||||
|
||||
class Meta:
|
||||
unique_together = ['voter', 'comment']
|
||||
verbose_name = _('comment vote')
|
||||
verbose_name_plural = _('comment votes')
|
||||
unique_together = ["voter", "comment"]
|
||||
verbose_name = _("comment vote")
|
||||
verbose_name_plural = _("comment votes")
|
||||
|
||||
|
||||
class CommentLock(models.Model):
|
||||
page = models.CharField(max_length=30, verbose_name=_('associated page'), db_index=True,
|
||||
validators=[comment_validator])
|
||||
page = models.CharField(
|
||||
max_length=30,
|
||||
verbose_name=_("associated page"),
|
||||
db_index=True,
|
||||
validators=[comment_validator],
|
||||
)
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('override_comment_lock', _('Override comment lock')),
|
||||
)
|
||||
permissions = (("override_comment_lock", _("Override comment lock")),)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.page)
|
||||
|
||||
|
||||
class Notification(models.Model):
|
||||
owner = models.ForeignKey(Profile, verbose_name=_('owner'), related_name="notifications", on_delete=CASCADE)
|
||||
time = models.DateTimeField(verbose_name=_('posted time'), auto_now_add=True)
|
||||
comment = models.ForeignKey(Comment, null=True, verbose_name=_('comment'), on_delete=CASCADE)
|
||||
read = models.BooleanField(verbose_name=_('read'), default=False)
|
||||
category = models.CharField(verbose_name=_('category'), max_length=10)
|
||||
html_link = models.TextField(default='', verbose_name=_('html link to comments, used for non-comments'), max_length=1000)
|
||||
author = models.ForeignKey(Profile, null=True, verbose_name=_('who trigger, used for non-comment'), on_delete=CASCADE)
|
||||
owner = models.ForeignKey(
|
||||
Profile,
|
||||
verbose_name=_("owner"),
|
||||
related_name="notifications",
|
||||
on_delete=CASCADE,
|
||||
)
|
||||
time = models.DateTimeField(verbose_name=_("posted time"), auto_now_add=True)
|
||||
comment = models.ForeignKey(
|
||||
Comment, null=True, verbose_name=_("comment"), on_delete=CASCADE
|
||||
)
|
||||
read = models.BooleanField(verbose_name=_("read"), default=False)
|
||||
category = models.CharField(verbose_name=_("category"), max_length=10)
|
||||
html_link = models.TextField(
|
||||
default="",
|
||||
verbose_name=_("html link to comments, used for non-comments"),
|
||||
max_length=1000,
|
||||
)
|
||||
author = models.ForeignKey(
|
||||
Profile,
|
||||
null=True,
|
||||
verbose_name=_("who trigger, used for non-comment"),
|
||||
on_delete=CASCADE,
|
||||
)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -10,7 +10,7 @@ from mptt.models import MPTTModel
|
|||
|
||||
from judge.models.profile import Organization, Profile
|
||||
|
||||
__all__ = ['MiscConfig', 'validate_regex', 'NavigationBar', 'BlogPost']
|
||||
__all__ = ["MiscConfig", "validate_regex", "NavigationBar", "BlogPost"]
|
||||
|
||||
|
||||
class MiscConfig(models.Model):
|
||||
|
@ -21,32 +21,40 @@ class MiscConfig(models.Model):
|
|||
return self.key
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('configuration item')
|
||||
verbose_name_plural = _('miscellaneous configuration')
|
||||
verbose_name = _("configuration item")
|
||||
verbose_name_plural = _("miscellaneous configuration")
|
||||
|
||||
|
||||
def validate_regex(regex):
|
||||
try:
|
||||
re.compile(regex, re.VERBOSE)
|
||||
except re.error as e:
|
||||
raise ValidationError('Invalid regex: %s' % e.message)
|
||||
raise ValidationError("Invalid regex: %s" % e.message)
|
||||
|
||||
|
||||
class NavigationBar(MPTTModel):
|
||||
class Meta:
|
||||
verbose_name = _('navigation item')
|
||||
verbose_name_plural = _('navigation bar')
|
||||
verbose_name = _("navigation item")
|
||||
verbose_name_plural = _("navigation bar")
|
||||
|
||||
class MPTTMeta:
|
||||
order_insertion_by = ['order']
|
||||
order_insertion_by = ["order"]
|
||||
|
||||
order = models.PositiveIntegerField(db_index=True, verbose_name=_('order'))
|
||||
key = models.CharField(max_length=10, unique=True, verbose_name=_('identifier'))
|
||||
label = models.CharField(max_length=20, verbose_name=_('label'))
|
||||
path = models.CharField(max_length=255, verbose_name=_('link path'))
|
||||
regex = models.TextField(verbose_name=_('highlight regex'), validators=[validate_regex])
|
||||
parent = TreeForeignKey('self', verbose_name=_('parent item'), null=True, blank=True,
|
||||
related_name='children', on_delete=models.CASCADE)
|
||||
order = models.PositiveIntegerField(db_index=True, verbose_name=_("order"))
|
||||
key = models.CharField(max_length=10, unique=True, verbose_name=_("identifier"))
|
||||
label = models.CharField(max_length=20, verbose_name=_("label"))
|
||||
path = models.CharField(max_length=255, verbose_name=_("link path"))
|
||||
regex = models.TextField(
|
||||
verbose_name=_("highlight regex"), validators=[validate_regex]
|
||||
)
|
||||
parent = TreeForeignKey(
|
||||
"self",
|
||||
verbose_name=_("parent item"),
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="children",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.label
|
||||
|
@ -63,46 +71,61 @@ class NavigationBar(MPTTModel):
|
|||
|
||||
|
||||
class BlogPost(models.Model):
|
||||
title = models.CharField(verbose_name=_('post title'), max_length=100)
|
||||
authors = models.ManyToManyField(Profile, verbose_name=_('authors'), blank=True)
|
||||
slug = models.SlugField(verbose_name=_('slug'))
|
||||
visible = models.BooleanField(verbose_name=_('public visibility'), default=False)
|
||||
sticky = models.BooleanField(verbose_name=_('sticky'), default=False)
|
||||
publish_on = models.DateTimeField(verbose_name=_('publish after'))
|
||||
content = models.TextField(verbose_name=_('post content'))
|
||||
summary = models.TextField(verbose_name=_('post summary'), blank=True)
|
||||
og_image = models.CharField(verbose_name=_('openGraph image'), default='', max_length=150, blank=True)
|
||||
organizations = models.ManyToManyField(Organization, blank=True, verbose_name=_('organizations'),
|
||||
help_text=_('If private, only these organizations may see the blog post.'))
|
||||
is_organization_private = models.BooleanField(verbose_name=_('private to organizations'), default=False)
|
||||
title = models.CharField(verbose_name=_("post title"), max_length=100)
|
||||
authors = models.ManyToManyField(Profile, verbose_name=_("authors"), blank=True)
|
||||
slug = models.SlugField(verbose_name=_("slug"))
|
||||
visible = models.BooleanField(verbose_name=_("public visibility"), default=False)
|
||||
sticky = models.BooleanField(verbose_name=_("sticky"), default=False)
|
||||
publish_on = models.DateTimeField(verbose_name=_("publish after"))
|
||||
content = models.TextField(verbose_name=_("post content"))
|
||||
summary = models.TextField(verbose_name=_("post summary"), blank=True)
|
||||
og_image = models.CharField(
|
||||
verbose_name=_("openGraph image"), default="", max_length=150, blank=True
|
||||
)
|
||||
organizations = models.ManyToManyField(
|
||||
Organization,
|
||||
blank=True,
|
||||
verbose_name=_("organizations"),
|
||||
help_text=_("If private, only these organizations may see the blog post."),
|
||||
)
|
||||
is_organization_private = models.BooleanField(
|
||||
verbose_name=_("private to organizations"), default=False
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('blog_post', args=(self.id, self.slug))
|
||||
return reverse("blog_post", args=(self.id, self.slug))
|
||||
|
||||
def can_see(self, user):
|
||||
if self.visible and self.publish_on <= timezone.now():
|
||||
if not self.is_organization_private:
|
||||
return True
|
||||
if user.is_authenticated and \
|
||||
self.organizations.filter(id__in=user.profile.organizations.all()).exists():
|
||||
if (
|
||||
user.is_authenticated
|
||||
and self.organizations.filter(
|
||||
id__in=user.profile.organizations.all()
|
||||
).exists()
|
||||
):
|
||||
return True
|
||||
if user.has_perm('judge.edit_all_post'):
|
||||
if user.has_perm("judge.edit_all_post"):
|
||||
return True
|
||||
return user.is_authenticated and self.authors.filter(id=user.profile.id).exists()
|
||||
return (
|
||||
user.is_authenticated and self.authors.filter(id=user.profile.id).exists()
|
||||
)
|
||||
|
||||
def is_editable_by(self, user):
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
if user.has_perm('judge.edit_all_post'):
|
||||
return True
|
||||
return user.has_perm('judge.change_blogpost') and self.authors.filter(id=user.profile.id).exists()
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
if user.has_perm("judge.edit_all_post"):
|
||||
return True
|
||||
return (
|
||||
user.has_perm("judge.change_blogpost")
|
||||
and self.authors.filter(id=user.profile.id).exists()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('edit_all_post', _('Edit all posts')),
|
||||
)
|
||||
verbose_name = _('blog post')
|
||||
verbose_name_plural = _('blog posts')
|
||||
permissions = (("edit_all_post", _("Edit all posts")),)
|
||||
verbose_name = _("blog post")
|
||||
verbose_name_plural = _("blog posts")
|
||||
|
|
|
@ -4,17 +4,31 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from judge.models.profile import Profile
|
||||
|
||||
__all__ = ['PrivateMessage', 'PrivateMessageThread']
|
||||
__all__ = ["PrivateMessage", "PrivateMessageThread"]
|
||||
|
||||
|
||||
class PrivateMessage(models.Model):
|
||||
title = models.CharField(verbose_name=_('message title'), max_length=50)
|
||||
content = models.TextField(verbose_name=_('message body'))
|
||||
sender = models.ForeignKey(Profile, verbose_name=_('sender'), related_name='sent_messages', on_delete=CASCADE)
|
||||
target = models.ForeignKey(Profile, verbose_name=_('target'), related_name='received_messages', on_delete=CASCADE)
|
||||
timestamp = models.DateTimeField(verbose_name=_('message timestamp'), auto_now_add=True)
|
||||
read = models.BooleanField(verbose_name=_('read'), default=False)
|
||||
title = models.CharField(verbose_name=_("message title"), max_length=50)
|
||||
content = models.TextField(verbose_name=_("message body"))
|
||||
sender = models.ForeignKey(
|
||||
Profile,
|
||||
verbose_name=_("sender"),
|
||||
related_name="sent_messages",
|
||||
on_delete=CASCADE,
|
||||
)
|
||||
target = models.ForeignKey(
|
||||
Profile,
|
||||
verbose_name=_("target"),
|
||||
related_name="received_messages",
|
||||
on_delete=CASCADE,
|
||||
)
|
||||
timestamp = models.DateTimeField(
|
||||
verbose_name=_("message timestamp"), auto_now_add=True
|
||||
)
|
||||
read = models.BooleanField(verbose_name=_("read"), default=False)
|
||||
|
||||
|
||||
class PrivateMessageThread(models.Model):
|
||||
messages = models.ManyToManyField(PrivateMessage, verbose_name=_('messages in the thread'))
|
||||
messages = models.ManyToManyField(
|
||||
PrivateMessage, verbose_name=_("messages in the thread")
|
||||
)
|
||||
|
|
|
@ -19,143 +19,286 @@ from judge.models.runtime import Language
|
|||
from judge.user_translations import gettext as user_gettext
|
||||
from judge.utils.raw_sql import RawSQLColumn, unique_together_left_join
|
||||
|
||||
__all__ = ['ProblemGroup', 'ProblemType', 'Problem', 'ProblemTranslation', 'ProblemClarification',
|
||||
'License', 'Solution', 'TranslatedProblemQuerySet', 'TranslatedProblemForeignKeyQuerySet']
|
||||
__all__ = [
|
||||
"ProblemGroup",
|
||||
"ProblemType",
|
||||
"Problem",
|
||||
"ProblemTranslation",
|
||||
"ProblemClarification",
|
||||
"License",
|
||||
"Solution",
|
||||
"TranslatedProblemQuerySet",
|
||||
"TranslatedProblemForeignKeyQuerySet",
|
||||
]
|
||||
|
||||
|
||||
class ProblemType(models.Model):
|
||||
name = models.CharField(max_length=20, verbose_name=_('problem category ID'), unique=True)
|
||||
full_name = models.CharField(max_length=100, verbose_name=_('problem category name'))
|
||||
name = models.CharField(
|
||||
max_length=20, verbose_name=_("problem category ID"), unique=True
|
||||
)
|
||||
full_name = models.CharField(
|
||||
max_length=100, verbose_name=_("problem category name")
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.full_name
|
||||
|
||||
class Meta:
|
||||
ordering = ['full_name']
|
||||
verbose_name = _('problem type')
|
||||
verbose_name_plural = _('problem types')
|
||||
ordering = ["full_name"]
|
||||
verbose_name = _("problem type")
|
||||
verbose_name_plural = _("problem types")
|
||||
|
||||
|
||||
class ProblemGroup(models.Model):
|
||||
name = models.CharField(max_length=20, verbose_name=_('problem group ID'), unique=True)
|
||||
full_name = models.CharField(max_length=100, verbose_name=_('problem group name'))
|
||||
name = models.CharField(
|
||||
max_length=20, verbose_name=_("problem group ID"), unique=True
|
||||
)
|
||||
full_name = models.CharField(max_length=100, verbose_name=_("problem group name"))
|
||||
|
||||
def __str__(self):
|
||||
return self.full_name
|
||||
|
||||
class Meta:
|
||||
ordering = ['full_name']
|
||||
verbose_name = _('problem group')
|
||||
verbose_name_plural = _('problem groups')
|
||||
ordering = ["full_name"]
|
||||
verbose_name = _("problem group")
|
||||
verbose_name_plural = _("problem groups")
|
||||
|
||||
|
||||
class License(models.Model):
|
||||
key = models.CharField(max_length=20, unique=True, verbose_name=_('key'),
|
||||
validators=[RegexValidator(r'^[-\w.]+$', r'License key must be ^[-\w.]+$')])
|
||||
link = models.CharField(max_length=256, verbose_name=_('link'))
|
||||
name = models.CharField(max_length=256, verbose_name=_('full name'))
|
||||
display = models.CharField(max_length=256, blank=True, verbose_name=_('short name'),
|
||||
help_text=_('Displayed on pages under this license'))
|
||||
icon = models.CharField(max_length=256, blank=True, verbose_name=_('icon'), help_text=_('URL to the icon'))
|
||||
text = models.TextField(verbose_name=_('license text'))
|
||||
key = models.CharField(
|
||||
max_length=20,
|
||||
unique=True,
|
||||
verbose_name=_("key"),
|
||||
validators=[RegexValidator(r"^[-\w.]+$", r"License key must be ^[-\w.]+$")],
|
||||
)
|
||||
link = models.CharField(max_length=256, verbose_name=_("link"))
|
||||
name = models.CharField(max_length=256, verbose_name=_("full name"))
|
||||
display = models.CharField(
|
||||
max_length=256,
|
||||
blank=True,
|
||||
verbose_name=_("short name"),
|
||||
help_text=_("Displayed on pages under this license"),
|
||||
)
|
||||
icon = models.CharField(
|
||||
max_length=256,
|
||||
blank=True,
|
||||
verbose_name=_("icon"),
|
||||
help_text=_("URL to the icon"),
|
||||
)
|
||||
text = models.TextField(verbose_name=_("license text"))
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('license', args=(self.key,))
|
||||
return reverse("license", args=(self.key,))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('license')
|
||||
verbose_name_plural = _('licenses')
|
||||
verbose_name = _("license")
|
||||
verbose_name_plural = _("licenses")
|
||||
|
||||
|
||||
class TranslatedProblemQuerySet(SearchQuerySet):
|
||||
def __init__(self, **kwargs):
|
||||
super(TranslatedProblemQuerySet, self).__init__(('code', 'name', 'description'), **kwargs)
|
||||
super(TranslatedProblemQuerySet, self).__init__(
|
||||
("code", "name", "description"), **kwargs
|
||||
)
|
||||
|
||||
def add_i18n_name(self, language):
|
||||
queryset = self._clone()
|
||||
alias = unique_together_left_join(queryset, ProblemTranslation, 'problem', 'language', language)
|
||||
return queryset.annotate(i18n_name=Coalesce(RawSQL('%s.name' % alias, ()), F('name'),
|
||||
output_field=models.CharField()))
|
||||
alias = unique_together_left_join(
|
||||
queryset, ProblemTranslation, "problem", "language", language
|
||||
)
|
||||
return queryset.annotate(
|
||||
i18n_name=Coalesce(
|
||||
RawSQL("%s.name" % alias, ()),
|
||||
F("name"),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class TranslatedProblemForeignKeyQuerySet(QuerySet):
|
||||
def add_problem_i18n_name(self, key, language, name_field=None):
|
||||
queryset = self._clone() if name_field is None else self.annotate(_name=F(name_field))
|
||||
alias = unique_together_left_join(queryset, ProblemTranslation, 'problem', 'language', language,
|
||||
parent_model=Problem)
|
||||
queryset = (
|
||||
self._clone() if name_field is None else self.annotate(_name=F(name_field))
|
||||
)
|
||||
alias = unique_together_left_join(
|
||||
queryset,
|
||||
ProblemTranslation,
|
||||
"problem",
|
||||
"language",
|
||||
language,
|
||||
parent_model=Problem,
|
||||
)
|
||||
# You must specify name_field if Problem is not yet joined into the QuerySet.
|
||||
kwargs = {key: Coalesce(RawSQL('%s.name' % alias, ()),
|
||||
F(name_field) if name_field else RawSQLColumn(Problem, 'name'),
|
||||
output_field=models.CharField())}
|
||||
kwargs = {
|
||||
key: Coalesce(
|
||||
RawSQL("%s.name" % alias, ()),
|
||||
F(name_field) if name_field else RawSQLColumn(Problem, "name"),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
}
|
||||
return queryset.annotate(**kwargs)
|
||||
|
||||
|
||||
class Problem(models.Model):
|
||||
code = models.CharField(max_length=20, verbose_name=_('problem code'), unique=True,
|
||||
validators=[RegexValidator('^[a-z0-9]+$', _('Problem code must be ^[a-z0-9]+$'))],
|
||||
help_text=_('A short, unique code for the problem, '
|
||||
'used in the url after /problem/'))
|
||||
name = models.CharField(max_length=100, verbose_name=_('problem name'), db_index=True,
|
||||
help_text=_('The full name of the problem, '
|
||||
'as shown in the problem list.'))
|
||||
description = models.TextField(verbose_name=_('problem body'))
|
||||
authors = models.ManyToManyField(Profile, verbose_name=_('creators'), blank=True, related_name='authored_problems',
|
||||
help_text=_('These users will be able to edit the problem, '
|
||||
'and be listed as authors.'))
|
||||
curators = models.ManyToManyField(Profile, verbose_name=_('curators'), blank=True, related_name='curated_problems',
|
||||
help_text=_('These users will be able to edit the problem, '
|
||||
'but not be listed as authors.'))
|
||||
testers = models.ManyToManyField(Profile, verbose_name=_('testers'), blank=True, related_name='tested_problems',
|
||||
help_text=_(
|
||||
'These users will be able to view the private problem, but not edit it.'))
|
||||
types = models.ManyToManyField(ProblemType, verbose_name=_('problem types'),
|
||||
help_text=_('The type of problem, '
|
||||
"as shown on the problem's page."))
|
||||
group = models.ForeignKey(ProblemGroup, verbose_name=_('problem group'), on_delete=CASCADE,
|
||||
help_text=_('The group of problem, shown under Category in the problem list.'))
|
||||
time_limit = models.FloatField(verbose_name=_('time limit'),
|
||||
help_text=_('The time limit for this problem, in seconds. '
|
||||
'Fractional seconds (e.g. 1.5) are supported.'),
|
||||
validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_TIME_LIMIT),
|
||||
MaxValueValidator(settings.DMOJ_PROBLEM_MAX_TIME_LIMIT)])
|
||||
memory_limit = models.PositiveIntegerField(verbose_name=_('memory limit'),
|
||||
help_text=_('The memory limit for this problem, in kilobytes '
|
||||
'(e.g. 64mb = 65536 kilobytes).'),
|
||||
validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_MEMORY_LIMIT),
|
||||
MaxValueValidator(settings.DMOJ_PROBLEM_MAX_MEMORY_LIMIT)])
|
||||
code = models.CharField(
|
||||
max_length=20,
|
||||
verbose_name=_("problem code"),
|
||||
unique=True,
|
||||
validators=[
|
||||
RegexValidator("^[a-z0-9]+$", _("Problem code must be ^[a-z0-9]+$"))
|
||||
],
|
||||
help_text=_(
|
||||
"A short, unique code for the problem, " "used in the url after /problem/"
|
||||
),
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
verbose_name=_("problem name"),
|
||||
db_index=True,
|
||||
help_text=_("The full name of the problem, " "as shown in the problem list."),
|
||||
)
|
||||
description = models.TextField(verbose_name=_("problem body"))
|
||||
authors = models.ManyToManyField(
|
||||
Profile,
|
||||
verbose_name=_("creators"),
|
||||
blank=True,
|
||||
related_name="authored_problems",
|
||||
help_text=_(
|
||||
"These users will be able to edit the problem, " "and be listed as authors."
|
||||
),
|
||||
)
|
||||
curators = models.ManyToManyField(
|
||||
Profile,
|
||||
verbose_name=_("curators"),
|
||||
blank=True,
|
||||
related_name="curated_problems",
|
||||
help_text=_(
|
||||
"These users will be able to edit the problem, "
|
||||
"but not be listed as authors."
|
||||
),
|
||||
)
|
||||
testers = models.ManyToManyField(
|
||||
Profile,
|
||||
verbose_name=_("testers"),
|
||||
blank=True,
|
||||
related_name="tested_problems",
|
||||
help_text=_(
|
||||
"These users will be able to view the private problem, but not edit it."
|
||||
),
|
||||
)
|
||||
types = models.ManyToManyField(
|
||||
ProblemType,
|
||||
verbose_name=_("problem types"),
|
||||
help_text=_("The type of problem, " "as shown on the problem's page."),
|
||||
)
|
||||
group = models.ForeignKey(
|
||||
ProblemGroup,
|
||||
verbose_name=_("problem group"),
|
||||
on_delete=CASCADE,
|
||||
help_text=_("The group of problem, shown under Category in the problem list."),
|
||||
)
|
||||
time_limit = models.FloatField(
|
||||
verbose_name=_("time limit"),
|
||||
help_text=_(
|
||||
"The time limit for this problem, in seconds. "
|
||||
"Fractional seconds (e.g. 1.5) are supported."
|
||||
),
|
||||
validators=[
|
||||
MinValueValidator(settings.DMOJ_PROBLEM_MIN_TIME_LIMIT),
|
||||
MaxValueValidator(settings.DMOJ_PROBLEM_MAX_TIME_LIMIT),
|
||||
],
|
||||
)
|
||||
memory_limit = models.PositiveIntegerField(
|
||||
verbose_name=_("memory limit"),
|
||||
help_text=_(
|
||||
"The memory limit for this problem, in kilobytes "
|
||||
"(e.g. 64mb = 65536 kilobytes)."
|
||||
),
|
||||
validators=[
|
||||
MinValueValidator(settings.DMOJ_PROBLEM_MIN_MEMORY_LIMIT),
|
||||
MaxValueValidator(settings.DMOJ_PROBLEM_MAX_MEMORY_LIMIT),
|
||||
],
|
||||
)
|
||||
short_circuit = models.BooleanField(default=False)
|
||||
points = models.FloatField(verbose_name=_('points'),
|
||||
help_text=_('Points awarded for problem completion. '
|
||||
"Points are displayed with a 'p' suffix if partial."),
|
||||
validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_PROBLEM_POINTS)])
|
||||
partial = models.BooleanField(verbose_name=_('allows partial points'), default=False)
|
||||
allowed_languages = models.ManyToManyField(Language, verbose_name=_('allowed languages'),
|
||||
help_text=_('List of allowed submission languages.'))
|
||||
is_public = models.BooleanField(verbose_name=_('publicly visible'), db_index=True, default=False)
|
||||
is_manually_managed = models.BooleanField(verbose_name=_('manually managed'), db_index=True, default=False,
|
||||
help_text=_('Whether judges should be allowed to manage data or not.'))
|
||||
date = models.DateTimeField(verbose_name=_('date of publishing'), null=True, blank=True, db_index=True,
|
||||
help_text=_("Doesn't have magic ability to auto-publish due to backward compatibility"))
|
||||
banned_users = models.ManyToManyField(Profile, verbose_name=_('personae non gratae'), blank=True,
|
||||
help_text=_('Bans the selected users from submitting to this problem.'))
|
||||
license = models.ForeignKey(License, null=True, blank=True, on_delete=SET_NULL,
|
||||
help_text=_('The license under which this problem is published.'))
|
||||
og_image = models.CharField(verbose_name=_('OpenGraph image'), max_length=150, blank=True)
|
||||
summary = models.TextField(blank=True, verbose_name=_('problem summary'),
|
||||
help_text=_('Plain-text, shown in meta description tag, e.g. for social media.'))
|
||||
user_count = models.IntegerField(verbose_name=_('number of users'), default=0,
|
||||
help_text=_('The number of users who solved the problem.'))
|
||||
ac_rate = models.FloatField(verbose_name=_('solve rate'), default=0)
|
||||
points = models.FloatField(
|
||||
verbose_name=_("points"),
|
||||
help_text=_(
|
||||
"Points awarded for problem completion. "
|
||||
"Points are displayed with a 'p' suffix if partial."
|
||||
),
|
||||
validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_PROBLEM_POINTS)],
|
||||
)
|
||||
partial = models.BooleanField(
|
||||
verbose_name=_("allows partial points"), default=False
|
||||
)
|
||||
allowed_languages = models.ManyToManyField(
|
||||
Language,
|
||||
verbose_name=_("allowed languages"),
|
||||
help_text=_("List of allowed submission languages."),
|
||||
)
|
||||
is_public = models.BooleanField(
|
||||
verbose_name=_("publicly visible"), db_index=True, default=False
|
||||
)
|
||||
is_manually_managed = models.BooleanField(
|
||||
verbose_name=_("manually managed"),
|
||||
db_index=True,
|
||||
default=False,
|
||||
help_text=_("Whether judges should be allowed to manage data or not."),
|
||||
)
|
||||
date = models.DateTimeField(
|
||||
verbose_name=_("date of publishing"),
|
||||
null=True,
|
||||
blank=True,
|
||||
db_index=True,
|
||||
help_text=_(
|
||||
"Doesn't have magic ability to auto-publish due to backward compatibility"
|
||||
),
|
||||
)
|
||||
banned_users = models.ManyToManyField(
|
||||
Profile,
|
||||
verbose_name=_("personae non gratae"),
|
||||
blank=True,
|
||||
help_text=_("Bans the selected users from submitting to this problem."),
|
||||
)
|
||||
license = models.ForeignKey(
|
||||
License,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=SET_NULL,
|
||||
help_text=_("The license under which this problem is published."),
|
||||
)
|
||||
og_image = models.CharField(
|
||||
verbose_name=_("OpenGraph image"), max_length=150, blank=True
|
||||
)
|
||||
summary = models.TextField(
|
||||
blank=True,
|
||||
verbose_name=_("problem summary"),
|
||||
help_text=_(
|
||||
"Plain-text, shown in meta description tag, e.g. for social media."
|
||||
),
|
||||
)
|
||||
user_count = models.IntegerField(
|
||||
verbose_name=_("number of users"),
|
||||
default=0,
|
||||
help_text=_("The number of users who solved the problem."),
|
||||
)
|
||||
ac_rate = models.FloatField(verbose_name=_("solve rate"), default=0)
|
||||
|
||||
objects = TranslatedProblemQuerySet.as_manager()
|
||||
tickets = GenericRelation('Ticket')
|
||||
tickets = GenericRelation("Ticket")
|
||||
|
||||
organizations = models.ManyToManyField(Organization, blank=True, verbose_name=_('organizations'),
|
||||
help_text=_('If private, only these organizations may see the problem.'))
|
||||
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 problem."),
|
||||
)
|
||||
is_organization_private = models.BooleanField(
|
||||
verbose_name=_("private to organizations"), default=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Problem, self).__init__(*args, **kwargs)
|
||||
|
@ -165,20 +308,30 @@ class Problem(models.Model):
|
|||
|
||||
@cached_property
|
||||
def types_list(self):
|
||||
return list(map(user_gettext, map(attrgetter('full_name'), self.types.all())))
|
||||
return list(map(user_gettext, map(attrgetter("full_name"), self.types.all())))
|
||||
|
||||
def languages_list(self):
|
||||
return self.allowed_languages.values_list('common_name', flat=True).distinct().order_by('common_name')
|
||||
return (
|
||||
self.allowed_languages.values_list("common_name", flat=True)
|
||||
.distinct()
|
||||
.order_by("common_name")
|
||||
)
|
||||
|
||||
def is_editor(self, profile):
|
||||
return (self.authors.filter(id=profile.id) | self.curators.filter(id=profile.id)).exists()
|
||||
return (
|
||||
self.authors.filter(id=profile.id) | self.curators.filter(id=profile.id)
|
||||
).exists()
|
||||
|
||||
def is_editable_by(self, user):
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
if user.has_perm('judge.edit_all_problem') or user.has_perm('judge.edit_public_problem') and self.is_public:
|
||||
if (
|
||||
user.has_perm("judge.edit_all_problem")
|
||||
or user.has_perm("judge.edit_public_problem")
|
||||
and self.is_public
|
||||
):
|
||||
return True
|
||||
return user.has_perm('judge.edit_own_problem') and self.is_editor(user.profile)
|
||||
return user.has_perm("judge.edit_own_problem") and self.is_editor(user.profile)
|
||||
|
||||
def is_accessible_by(self, user):
|
||||
# Problem is public.
|
||||
|
@ -188,23 +341,24 @@ class Problem(models.Model):
|
|||
return True
|
||||
|
||||
# If the user can see all organization private problems.
|
||||
if user.has_perm('judge.see_organization_problem'):
|
||||
if user.has_perm("judge.see_organization_problem"):
|
||||
return True
|
||||
|
||||
# If the user is in the organization.
|
||||
if user.is_authenticated and \
|
||||
self.organizations.filter(id__in=user.profile.organizations.all()):
|
||||
if user.is_authenticated and self.organizations.filter(
|
||||
id__in=user.profile.organizations.all()
|
||||
):
|
||||
return True
|
||||
|
||||
# If the user can view all problems.
|
||||
if user.has_perm('judge.see_private_problem'):
|
||||
if user.has_perm("judge.see_private_problem"):
|
||||
return True
|
||||
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
|
||||
# If the user authored the problem or is a curator.
|
||||
if user.has_perm('judge.edit_own_problem') and self.is_editor(user.profile):
|
||||
if user.has_perm("judge.edit_own_problem") and self.is_editor(user.profile):
|
||||
return True
|
||||
|
||||
# If user is a tester.
|
||||
|
@ -216,11 +370,18 @@ class Problem(models.Model):
|
|||
if current is None:
|
||||
return False
|
||||
from judge.models import ContestProblem
|
||||
return ContestProblem.objects.filter(problem_id=self.id, contest__users__id=current).exists()
|
||||
|
||||
return ContestProblem.objects.filter(
|
||||
problem_id=self.id, contest__users__id=current
|
||||
).exists()
|
||||
|
||||
def is_subs_manageable_by(self, user):
|
||||
return user.is_staff and user.has_perm('judge.rejudge_submission') and self.is_editable_by(user)
|
||||
|
||||
return (
|
||||
user.is_staff
|
||||
and user.has_perm("judge.rejudge_submission")
|
||||
and self.is_editable_by(user)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_visible_problems(cls, user):
|
||||
# Do unauthenticated check here so we can skip authentication checks later on.
|
||||
|
@ -235,15 +396,18 @@ class Problem(models.Model):
|
|||
# - is_public problems
|
||||
# - not is_organization_private or in organization or `judge.see_organization_problem`
|
||||
# - author or curator or tester
|
||||
queryset = cls.objects.defer('description')
|
||||
queryset = cls.objects.defer("description")
|
||||
|
||||
if not (user.has_perm('judge.see_private_problem') or user.has_perm('judge.edit_all_problem')):
|
||||
if not (
|
||||
user.has_perm("judge.see_private_problem")
|
||||
or user.has_perm("judge.edit_all_problem")
|
||||
):
|
||||
q = Q(is_public=True)
|
||||
if not user.has_perm('judge.see_organization_problem'):
|
||||
if not user.has_perm("judge.see_organization_problem"):
|
||||
# Either not organization private or in the organization.
|
||||
q &= (
|
||||
Q(is_organization_private=False) |
|
||||
Q(is_organization_private=True, organizations__in=user.profile.organizations.all())
|
||||
q &= Q(is_organization_private=False) | Q(
|
||||
is_organization_private=True,
|
||||
organizations__in=user.profile.organizations.all(),
|
||||
)
|
||||
|
||||
# Authors, curators, and testers should always have access, so OR at the very end.
|
||||
|
@ -256,40 +420,46 @@ class Problem(models.Model):
|
|||
|
||||
@classmethod
|
||||
def get_public_problems(cls):
|
||||
return cls.objects.filter(is_public=True, is_organization_private=False).defer('description')
|
||||
return cls.objects.filter(is_public=True, is_organization_private=False).defer(
|
||||
"description"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('problem_detail', args=(self.code,))
|
||||
return reverse("problem_detail", args=(self.code,))
|
||||
|
||||
@cached_property
|
||||
def author_ids(self):
|
||||
return self.authors.values_list('id', flat=True)
|
||||
return self.authors.values_list("id", flat=True)
|
||||
|
||||
@cached_property
|
||||
def editor_ids(self):
|
||||
return self.author_ids | self.curators.values_list('id', flat=True)
|
||||
return self.author_ids | self.curators.values_list("id", flat=True)
|
||||
|
||||
@cached_property
|
||||
def tester_ids(self):
|
||||
return self.testers.values_list('id', flat=True)
|
||||
return self.testers.values_list("id", flat=True)
|
||||
|
||||
@cached_property
|
||||
def usable_common_names(self):
|
||||
return set(self.usable_languages.values_list('common_name', flat=True))
|
||||
return set(self.usable_languages.values_list("common_name", flat=True))
|
||||
|
||||
@property
|
||||
def usable_languages(self):
|
||||
return self.allowed_languages.filter(judges__in=self.judges.filter(online=True)).distinct()
|
||||
return self.allowed_languages.filter(
|
||||
judges__in=self.judges.filter(online=True)
|
||||
).distinct()
|
||||
|
||||
def translated_name(self, language):
|
||||
if language in self._translated_name_cache:
|
||||
return self._translated_name_cache[language]
|
||||
# Hits database despite prefetch_related.
|
||||
try:
|
||||
name = self.translations.filter(language=language).values_list('name', flat=True)[0]
|
||||
name = self.translations.filter(language=language).values_list(
|
||||
"name", flat=True
|
||||
)[0]
|
||||
except IndexError:
|
||||
name = self.name
|
||||
self._translated_name_cache[language] = name
|
||||
|
@ -310,12 +480,23 @@ class Problem(models.Model):
|
|||
return ProblemClarification.objects.filter(problem=self)
|
||||
|
||||
def update_stats(self):
|
||||
self.user_count = self.submission_set.filter(points__gte=self.points, result='AC',
|
||||
user__is_unlisted=False).values('user').distinct().count()
|
||||
self.user_count = (
|
||||
self.submission_set.filter(
|
||||
points__gte=self.points, result="AC", user__is_unlisted=False
|
||||
)
|
||||
.values("user")
|
||||
.distinct()
|
||||
.count()
|
||||
)
|
||||
submissions = self.submission_set.count()
|
||||
if submissions:
|
||||
self.ac_rate = 100.0 * self.submission_set.filter(points__gte=self.points, result='AC',
|
||||
user__is_unlisted=False).count() / submissions
|
||||
self.ac_rate = (
|
||||
100.0
|
||||
* self.submission_set.filter(
|
||||
points__gte=self.points, result="AC", user__is_unlisted=False
|
||||
).count()
|
||||
/ submissions
|
||||
)
|
||||
else:
|
||||
self.ac_rate = 0
|
||||
self.save()
|
||||
|
@ -324,9 +505,13 @@ class Problem(models.Model):
|
|||
|
||||
def _get_limits(self, key):
|
||||
global_limit = getattr(self, key)
|
||||
limits = {limit['language_id']: (limit['language__name'], limit[key])
|
||||
for limit in self.language_limits.values('language_id', 'language__name', key)
|
||||
if limit[key] != global_limit}
|
||||
limits = {
|
||||
limit["language_id"]: (limit["language__name"], limit[key])
|
||||
for limit in self.language_limits.values(
|
||||
"language_id", "language__name", key
|
||||
)
|
||||
if limit[key] != global_limit
|
||||
}
|
||||
limit_ids = set(limits.keys())
|
||||
common = []
|
||||
|
||||
|
@ -346,21 +531,21 @@ class Problem(models.Model):
|
|||
|
||||
@property
|
||||
def language_time_limit(self):
|
||||
key = 'problem_tls:%d' % self.id
|
||||
key = "problem_tls:%d" % self.id
|
||||
result = cache.get(key)
|
||||
if result is not None:
|
||||
return result
|
||||
result = self._get_limits('time_limit')
|
||||
result = self._get_limits("time_limit")
|
||||
cache.set(key, result)
|
||||
return result
|
||||
|
||||
@property
|
||||
def language_memory_limit(self):
|
||||
key = 'problem_mls:%d' % self.id
|
||||
key = "problem_mls:%d" % self.id
|
||||
result = cache.get(key)
|
||||
if result is not None:
|
||||
return result
|
||||
result = self._get_limits('memory_limit')
|
||||
result = self._get_limits("memory_limit")
|
||||
cache.set(key, result)
|
||||
return result
|
||||
|
||||
|
@ -395,105 +580,143 @@ class Problem(models.Model):
|
|||
return False
|
||||
|
||||
# If the user has a full AC submission to the problem (solved the problem).
|
||||
return self.submission_set.filter(user=user.profile, result='AC', points=F('problem__points')).exists()
|
||||
return self.submission_set.filter(
|
||||
user=user.profile, result="AC", points=F("problem__points")
|
||||
).exists()
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('see_private_problem', 'See hidden problems'),
|
||||
('edit_own_problem', 'Edit own problems'),
|
||||
('edit_all_problem', 'Edit all problems'),
|
||||
('edit_public_problem', 'Edit all public problems'),
|
||||
('clone_problem', 'Clone problem'),
|
||||
('change_public_visibility', 'Change is_public field'),
|
||||
('change_manually_managed', 'Change is_manually_managed field'),
|
||||
('see_organization_problem', 'See organization-private problems'),
|
||||
('suggest_problem_changes', 'Suggest changes to problem'),
|
||||
("see_private_problem", "See hidden problems"),
|
||||
("edit_own_problem", "Edit own problems"),
|
||||
("edit_all_problem", "Edit all problems"),
|
||||
("edit_public_problem", "Edit all public problems"),
|
||||
("clone_problem", "Clone problem"),
|
||||
("change_public_visibility", "Change is_public field"),
|
||||
("change_manually_managed", "Change is_manually_managed field"),
|
||||
("see_organization_problem", "See organization-private problems"),
|
||||
("suggest_problem_changes", "Suggest changes to problem"),
|
||||
)
|
||||
verbose_name = _('problem')
|
||||
verbose_name_plural = _('problems')
|
||||
verbose_name = _("problem")
|
||||
verbose_name_plural = _("problems")
|
||||
|
||||
|
||||
class ProblemTranslation(models.Model):
|
||||
problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='translations', on_delete=CASCADE)
|
||||
language = models.CharField(verbose_name=_('language'), max_length=7, choices=settings.LANGUAGES)
|
||||
name = models.CharField(verbose_name=_('translated name'), max_length=100, db_index=True)
|
||||
description = models.TextField(verbose_name=_('translated description'))
|
||||
problem = models.ForeignKey(
|
||||
Problem,
|
||||
verbose_name=_("problem"),
|
||||
related_name="translations",
|
||||
on_delete=CASCADE,
|
||||
)
|
||||
language = models.CharField(
|
||||
verbose_name=_("language"), max_length=7, choices=settings.LANGUAGES
|
||||
)
|
||||
name = models.CharField(
|
||||
verbose_name=_("translated name"), max_length=100, db_index=True
|
||||
)
|
||||
description = models.TextField(verbose_name=_("translated description"))
|
||||
|
||||
class Meta:
|
||||
unique_together = ('problem', 'language')
|
||||
verbose_name = _('problem translation')
|
||||
verbose_name_plural = _('problem translations')
|
||||
unique_together = ("problem", "language")
|
||||
verbose_name = _("problem translation")
|
||||
verbose_name_plural = _("problem translations")
|
||||
|
||||
|
||||
class ProblemClarification(models.Model):
|
||||
problem = models.ForeignKey(Problem, 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)
|
||||
problem = models.ForeignKey(
|
||||
Problem, 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
|
||||
)
|
||||
|
||||
|
||||
class LanguageLimit(models.Model):
|
||||
problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='language_limits', on_delete=CASCADE)
|
||||
language = models.ForeignKey(Language, verbose_name=_('language'), on_delete=CASCADE)
|
||||
time_limit = models.FloatField(verbose_name=_('time limit'),
|
||||
validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_TIME_LIMIT),
|
||||
MaxValueValidator(settings.DMOJ_PROBLEM_MAX_TIME_LIMIT)])
|
||||
memory_limit = models.IntegerField(verbose_name=_('memory limit'),
|
||||
validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_MEMORY_LIMIT),
|
||||
MaxValueValidator(settings.DMOJ_PROBLEM_MAX_MEMORY_LIMIT)])
|
||||
problem = models.ForeignKey(
|
||||
Problem,
|
||||
verbose_name=_("problem"),
|
||||
related_name="language_limits",
|
||||
on_delete=CASCADE,
|
||||
)
|
||||
language = models.ForeignKey(
|
||||
Language, verbose_name=_("language"), on_delete=CASCADE
|
||||
)
|
||||
time_limit = models.FloatField(
|
||||
verbose_name=_("time limit"),
|
||||
validators=[
|
||||
MinValueValidator(settings.DMOJ_PROBLEM_MIN_TIME_LIMIT),
|
||||
MaxValueValidator(settings.DMOJ_PROBLEM_MAX_TIME_LIMIT),
|
||||
],
|
||||
)
|
||||
memory_limit = models.IntegerField(
|
||||
verbose_name=_("memory limit"),
|
||||
validators=[
|
||||
MinValueValidator(settings.DMOJ_PROBLEM_MIN_MEMORY_LIMIT),
|
||||
MaxValueValidator(settings.DMOJ_PROBLEM_MAX_MEMORY_LIMIT),
|
||||
],
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('problem', 'language')
|
||||
verbose_name = _('language-specific resource limit')
|
||||
verbose_name_plural = _('language-specific resource limits')
|
||||
unique_together = ("problem", "language")
|
||||
verbose_name = _("language-specific resource limit")
|
||||
verbose_name_plural = _("language-specific resource limits")
|
||||
|
||||
|
||||
class Solution(models.Model):
|
||||
problem = models.OneToOneField(Problem, on_delete=SET_NULL, verbose_name=_('associated problem'),
|
||||
null=True, blank=True, related_name='solution')
|
||||
is_public = models.BooleanField(verbose_name=_('public visibility'), default=False)
|
||||
publish_on = models.DateTimeField(verbose_name=_('publish date'))
|
||||
authors = models.ManyToManyField(Profile, verbose_name=_('authors'), blank=True)
|
||||
content = models.TextField(verbose_name=_('editorial content'))
|
||||
problem = models.OneToOneField(
|
||||
Problem,
|
||||
on_delete=SET_NULL,
|
||||
verbose_name=_("associated problem"),
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="solution",
|
||||
)
|
||||
is_public = models.BooleanField(verbose_name=_("public visibility"), default=False)
|
||||
publish_on = models.DateTimeField(verbose_name=_("publish date"))
|
||||
authors = models.ManyToManyField(Profile, verbose_name=_("authors"), blank=True)
|
||||
content = models.TextField(verbose_name=_("editorial content"))
|
||||
|
||||
def get_absolute_url(self):
|
||||
problem = self.problem
|
||||
if problem is None:
|
||||
return reverse('home')
|
||||
return reverse("home")
|
||||
else:
|
||||
return reverse('problem_editorial', args=[problem.code])
|
||||
return reverse("problem_editorial", args=[problem.code])
|
||||
|
||||
def __str__(self):
|
||||
return _('Editorial for %s') % self.problem.name
|
||||
return _("Editorial for %s") % self.problem.name
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('see_private_solution', 'See hidden solutions'),
|
||||
)
|
||||
verbose_name = _('solution')
|
||||
verbose_name_plural = _('solutions')
|
||||
permissions = (("see_private_solution", "See hidden solutions"),)
|
||||
verbose_name = _("solution")
|
||||
verbose_name_plural = _("solutions")
|
||||
|
||||
|
||||
class ProblemPointsVote(models.Model):
|
||||
points = models.IntegerField(
|
||||
verbose_name=_('proposed point value'),
|
||||
help_text=_('The amount of points you think this problem deserves.'),
|
||||
verbose_name=_("proposed point value"),
|
||||
help_text=_("The amount of points you think this problem deserves."),
|
||||
validators=[
|
||||
MinValueValidator(100),
|
||||
MaxValueValidator(600),
|
||||
],
|
||||
)
|
||||
|
||||
voter = models.ForeignKey(Profile, related_name='problem_points_votes', on_delete=CASCADE, db_index=True)
|
||||
problem = models.ForeignKey(Problem, related_name='problem_points_votes', on_delete=CASCADE, db_index=True)
|
||||
voter = models.ForeignKey(
|
||||
Profile, related_name="problem_points_votes", on_delete=CASCADE, db_index=True
|
||||
)
|
||||
problem = models.ForeignKey(
|
||||
Problem, related_name="problem_points_votes", on_delete=CASCADE, db_index=True
|
||||
)
|
||||
vote_time = models.DateTimeField(
|
||||
verbose_name=_('The time this vote was cast'),
|
||||
verbose_name=_("The time this vote was cast"),
|
||||
auto_now_add=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('vote')
|
||||
verbose_name_plural = _('votes')
|
||||
verbose_name = _("vote")
|
||||
verbose_name_plural = _("votes")
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.voter}: {self.points} for {self.problem.code}'
|
||||
return f"{self.voter}: {self.points} for {self.problem.code}"
|
||||
|
|
|
@ -9,7 +9,13 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from judge.utils.problem_data import ProblemDataStorage, get_file_cachekey
|
||||
|
||||
__all__ = ['problem_data_storage', 'problem_directory_file', 'ProblemData', 'ProblemTestCase', 'CHECKERS']
|
||||
__all__ = [
|
||||
"problem_data_storage",
|
||||
"problem_directory_file",
|
||||
"ProblemData",
|
||||
"ProblemTestCase",
|
||||
"CHECKERS",
|
||||
]
|
||||
|
||||
problem_data_storage = ProblemDataStorage()
|
||||
|
||||
|
@ -23,52 +29,83 @@ def problem_directory_file(data, filename):
|
|||
|
||||
|
||||
CHECKERS = (
|
||||
('standard', _('Standard')),
|
||||
('floats', _('Floats')),
|
||||
('floatsabs', _('Floats (absolute)')),
|
||||
('floatsrel', _('Floats (relative)')),
|
||||
('rstripped', _('Non-trailing spaces')),
|
||||
('sorted', _('Unordered')),
|
||||
('identical', _('Byte identical')),
|
||||
('linecount', _('Line-by-line')),
|
||||
('custom', _('Custom checker (PY)')),
|
||||
('customval', _('Custom validator (CPP)')),
|
||||
('interact', _('Interactive')),
|
||||
("standard", _("Standard")),
|
||||
("floats", _("Floats")),
|
||||
("floatsabs", _("Floats (absolute)")),
|
||||
("floatsrel", _("Floats (relative)")),
|
||||
("rstripped", _("Non-trailing spaces")),
|
||||
("sorted", _("Unordered")),
|
||||
("identical", _("Byte identical")),
|
||||
("linecount", _("Line-by-line")),
|
||||
("custom", _("Custom checker (PY)")),
|
||||
("customval", _("Custom validator (CPP)")),
|
||||
("interact", _("Interactive")),
|
||||
)
|
||||
|
||||
|
||||
class ProblemData(models.Model):
|
||||
problem = models.OneToOneField('Problem', verbose_name=_('problem'), related_name='data_files',
|
||||
on_delete=models.CASCADE)
|
||||
zipfile = models.FileField(verbose_name=_('data zip file'), storage=problem_data_storage, null=True, blank=True,
|
||||
upload_to=problem_directory_file)
|
||||
generator = models.FileField(verbose_name=_('generator file'), storage=problem_data_storage, null=True, blank=True,
|
||||
upload_to=problem_directory_file)
|
||||
output_prefix = models.IntegerField(verbose_name=_('output prefix length'), blank=True, null=True)
|
||||
output_limit = models.IntegerField(verbose_name=_('output limit length'), blank=True, null=True)
|
||||
feedback = models.TextField(verbose_name=_('init.yml generation feedback'), blank=True)
|
||||
checker = models.CharField(max_length=10, verbose_name=_('checker'), choices=CHECKERS, blank=True)
|
||||
checker_args = models.TextField(verbose_name=_('checker arguments'), blank=True,
|
||||
help_text=_('checker arguments as a JSON object'))
|
||||
custom_checker = models.FileField(verbose_name=_('custom checker file'),
|
||||
storage=problem_data_storage,
|
||||
null=True,
|
||||
blank=True,
|
||||
upload_to=problem_directory_file,
|
||||
validators=[FileExtensionValidator(allowed_extensions=['py'])])
|
||||
custom_validator = models.FileField(verbose_name=_('custom validator file'),
|
||||
storage=problem_data_storage,
|
||||
null=True,
|
||||
blank=True,
|
||||
upload_to=problem_directory_file,
|
||||
validators=[FileExtensionValidator(allowed_extensions=['cpp'])])
|
||||
interactive_judge = models.FileField(verbose_name=_('interactive judge'),
|
||||
storage=problem_data_storage,
|
||||
null=True,
|
||||
blank=True,
|
||||
upload_to=problem_directory_file,
|
||||
validators=[FileExtensionValidator(allowed_extensions=['cpp'])])
|
||||
|
||||
problem = models.OneToOneField(
|
||||
"Problem",
|
||||
verbose_name=_("problem"),
|
||||
related_name="data_files",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
zipfile = models.FileField(
|
||||
verbose_name=_("data zip file"),
|
||||
storage=problem_data_storage,
|
||||
null=True,
|
||||
blank=True,
|
||||
upload_to=problem_directory_file,
|
||||
)
|
||||
generator = models.FileField(
|
||||
verbose_name=_("generator file"),
|
||||
storage=problem_data_storage,
|
||||
null=True,
|
||||
blank=True,
|
||||
upload_to=problem_directory_file,
|
||||
)
|
||||
output_prefix = models.IntegerField(
|
||||
verbose_name=_("output prefix length"), blank=True, null=True
|
||||
)
|
||||
output_limit = models.IntegerField(
|
||||
verbose_name=_("output limit length"), blank=True, null=True
|
||||
)
|
||||
feedback = models.TextField(
|
||||
verbose_name=_("init.yml generation feedback"), blank=True
|
||||
)
|
||||
checker = models.CharField(
|
||||
max_length=10, verbose_name=_("checker"), choices=CHECKERS, blank=True
|
||||
)
|
||||
checker_args = models.TextField(
|
||||
verbose_name=_("checker arguments"),
|
||||
blank=True,
|
||||
help_text=_("checker arguments as a JSON object"),
|
||||
)
|
||||
custom_checker = models.FileField(
|
||||
verbose_name=_("custom checker file"),
|
||||
storage=problem_data_storage,
|
||||
null=True,
|
||||
blank=True,
|
||||
upload_to=problem_directory_file,
|
||||
validators=[FileExtensionValidator(allowed_extensions=["py"])],
|
||||
)
|
||||
custom_validator = models.FileField(
|
||||
verbose_name=_("custom validator file"),
|
||||
storage=problem_data_storage,
|
||||
null=True,
|
||||
blank=True,
|
||||
upload_to=problem_directory_file,
|
||||
validators=[FileExtensionValidator(allowed_extensions=["cpp"])],
|
||||
)
|
||||
interactive_judge = models.FileField(
|
||||
verbose_name=_("interactive judge"),
|
||||
storage=problem_data_storage,
|
||||
null=True,
|
||||
blank=True,
|
||||
upload_to=problem_directory_file,
|
||||
validators=[FileExtensionValidator(allowed_extensions=["cpp"])],
|
||||
)
|
||||
|
||||
__original_zipfile = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
@ -78,10 +115,13 @@ class ProblemData(models.Model):
|
|||
def save(self, *args, **kwargs):
|
||||
# Delete caches
|
||||
if self.__original_zipfile:
|
||||
try:
|
||||
try:
|
||||
files = ZipFile(self.__original_zipfile.path).namelist()
|
||||
for file in files:
|
||||
cache_key = 'problem_archive:%s:%s' % (self.problem.code, get_file_cachekey(file))
|
||||
cache_key = "problem_archive:%s:%s" % (
|
||||
self.problem.code,
|
||||
get_file_cachekey(file),
|
||||
)
|
||||
cache.delete(cache_key)
|
||||
except BadZipFile:
|
||||
pass
|
||||
|
@ -90,7 +130,7 @@ class ProblemData(models.Model):
|
|||
return super(ProblemData, self).save(*args, **kwargs)
|
||||
|
||||
def has_yml(self):
|
||||
return problem_data_storage.exists('%s/init.yml' % self.problem.code)
|
||||
return problem_data_storage.exists("%s/init.yml" % self.problem.code)
|
||||
|
||||
def _update_code(self, original, new):
|
||||
try:
|
||||
|
@ -103,31 +143,60 @@ class ProblemData(models.Model):
|
|||
if self.generator:
|
||||
self.generator.name = _problem_directory_file(new, self.generator.name)
|
||||
if self.custom_checker:
|
||||
self.custom_checker.name = _problem_directory_file(new, self.custom_checker.name)
|
||||
self.custom_checker.name = _problem_directory_file(
|
||||
new, self.custom_checker.name
|
||||
)
|
||||
if self.custom_checker:
|
||||
self.custom_checker.name = _problem_directory_file(new, self.custom_checker.name)
|
||||
self.custom_checker.name = _problem_directory_file(
|
||||
new, self.custom_checker.name
|
||||
)
|
||||
if self.custom_validator:
|
||||
self.custom_validator.name = _problem_directory_file(new, self.custom_validator.name)
|
||||
self.custom_validator.name = _problem_directory_file(
|
||||
new, self.custom_validator.name
|
||||
)
|
||||
self.save()
|
||||
|
||||
_update_code.alters_data = True
|
||||
|
||||
|
||||
class ProblemTestCase(models.Model):
|
||||
dataset = models.ForeignKey('Problem', verbose_name=_('problem data set'), related_name='cases',
|
||||
on_delete=models.CASCADE)
|
||||
order = models.IntegerField(verbose_name=_('case position'))
|
||||
type = models.CharField(max_length=1, verbose_name=_('case type'),
|
||||
choices=(('C', _('Normal case')),
|
||||
('S', _('Batch start')),
|
||||
('E', _('Batch end'))),
|
||||
default='C')
|
||||
input_file = models.CharField(max_length=100, verbose_name=_('input file name'), blank=True)
|
||||
output_file = models.CharField(max_length=100, verbose_name=_('output file name'), blank=True)
|
||||
generator_args = models.TextField(verbose_name=_('generator arguments'), blank=True)
|
||||
points = models.IntegerField(verbose_name=_('point value'), blank=True, null=True)
|
||||
is_pretest = models.BooleanField(verbose_name=_('case is pretest?'))
|
||||
output_prefix = models.IntegerField(verbose_name=_('output prefix length'), blank=True, null=True)
|
||||
output_limit = models.IntegerField(verbose_name=_('output limit length'), blank=True, null=True)
|
||||
checker = models.CharField(max_length=10, verbose_name=_('checker'), choices=CHECKERS, blank=True)
|
||||
checker_args = models.TextField(verbose_name=_('checker arguments'), blank=True,
|
||||
help_text=_('checker arguments as a JSON object'))
|
||||
dataset = models.ForeignKey(
|
||||
"Problem",
|
||||
verbose_name=_("problem data set"),
|
||||
related_name="cases",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
order = models.IntegerField(verbose_name=_("case position"))
|
||||
type = models.CharField(
|
||||
max_length=1,
|
||||
verbose_name=_("case type"),
|
||||
choices=(
|
||||
("C", _("Normal case")),
|
||||
("S", _("Batch start")),
|
||||
("E", _("Batch end")),
|
||||
),
|
||||
default="C",
|
||||
)
|
||||
input_file = models.CharField(
|
||||
max_length=100, verbose_name=_("input file name"), blank=True
|
||||
)
|
||||
output_file = models.CharField(
|
||||
max_length=100, verbose_name=_("output file name"), blank=True
|
||||
)
|
||||
generator_args = models.TextField(verbose_name=_("generator arguments"), blank=True)
|
||||
points = models.IntegerField(verbose_name=_("point value"), blank=True, null=True)
|
||||
is_pretest = models.BooleanField(verbose_name=_("case is pretest?"))
|
||||
output_prefix = models.IntegerField(
|
||||
verbose_name=_("output prefix length"), blank=True, null=True
|
||||
)
|
||||
output_limit = models.IntegerField(
|
||||
verbose_name=_("output limit length"), blank=True, null=True
|
||||
)
|
||||
checker = models.CharField(
|
||||
max_length=10, verbose_name=_("checker"), choices=CHECKERS, blank=True
|
||||
)
|
||||
checker_args = models.TextField(
|
||||
verbose_name=_("checker arguments"),
|
||||
blank=True,
|
||||
help_text=_("checker arguments as a JSON object"),
|
||||
)
|
||||
|
|
|
@ -16,7 +16,7 @@ from judge.models.choices import ACE_THEMES, MATH_ENGINES_CHOICES, TIMEZONE
|
|||
from judge.models.runtime import Language
|
||||
from judge.ratings import rating_class
|
||||
|
||||
__all__ = ['Organization', 'Profile', 'OrganizationRequest', 'Friend']
|
||||
__all__ = ["Organization", "Profile", "OrganizationRequest", "Friend"]
|
||||
|
||||
|
||||
class EncryptedNullCharField(EncryptedCharField):
|
||||
|
@ -27,28 +27,65 @@ class EncryptedNullCharField(EncryptedCharField):
|
|||
|
||||
|
||||
class Organization(models.Model):
|
||||
name = models.CharField(max_length=128, verbose_name=_('organization title'))
|
||||
slug = models.SlugField(max_length=128, verbose_name=_('organization slug'),
|
||||
help_text=_('Organization name shown in URL'))
|
||||
short_name = models.CharField(max_length=20, verbose_name=_('short name'),
|
||||
help_text=_('Displayed beside user name during contests'))
|
||||
about = models.TextField(verbose_name=_('organization description'))
|
||||
registrant = models.ForeignKey('Profile', verbose_name=_('registrant'), on_delete=models.CASCADE,
|
||||
related_name='registrant+', help_text=_('User who registered this organization'))
|
||||
admins = models.ManyToManyField('Profile', verbose_name=_('administrators'), related_name='admin_of',
|
||||
help_text=_('Those who can edit this organization'))
|
||||
creation_date = models.DateTimeField(verbose_name=_('creation date'), auto_now_add=True)
|
||||
is_open = models.BooleanField(verbose_name=_('is open organization?'),
|
||||
help_text=_('Allow joining organization'), default=True)
|
||||
slots = models.IntegerField(verbose_name=_('maximum size'), null=True, blank=True,
|
||||
help_text=_('Maximum amount of users in this organization, '
|
||||
'only applicable to private organizations'))
|
||||
access_code = models.CharField(max_length=7, help_text=_('Student access code'),
|
||||
verbose_name=_('access code'), null=True, 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 '
|
||||
'viewing the organization.'))
|
||||
name = models.CharField(max_length=128, verbose_name=_("organization title"))
|
||||
slug = models.SlugField(
|
||||
max_length=128,
|
||||
verbose_name=_("organization slug"),
|
||||
help_text=_("Organization name shown in URL"),
|
||||
)
|
||||
short_name = models.CharField(
|
||||
max_length=20,
|
||||
verbose_name=_("short name"),
|
||||
help_text=_("Displayed beside user name during contests"),
|
||||
)
|
||||
about = models.TextField(verbose_name=_("organization description"))
|
||||
registrant = models.ForeignKey(
|
||||
"Profile",
|
||||
verbose_name=_("registrant"),
|
||||
on_delete=models.CASCADE,
|
||||
related_name="registrant+",
|
||||
help_text=_("User who registered this organization"),
|
||||
)
|
||||
admins = models.ManyToManyField(
|
||||
"Profile",
|
||||
verbose_name=_("administrators"),
|
||||
related_name="admin_of",
|
||||
help_text=_("Those who can edit this organization"),
|
||||
)
|
||||
creation_date = models.DateTimeField(
|
||||
verbose_name=_("creation date"), auto_now_add=True
|
||||
)
|
||||
is_open = models.BooleanField(
|
||||
verbose_name=_("is open organization?"),
|
||||
help_text=_("Allow joining organization"),
|
||||
default=True,
|
||||
)
|
||||
slots = models.IntegerField(
|
||||
verbose_name=_("maximum size"),
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_(
|
||||
"Maximum amount of users in this organization, "
|
||||
"only applicable to private organizations"
|
||||
),
|
||||
)
|
||||
access_code = models.CharField(
|
||||
max_length=7,
|
||||
help_text=_("Student access code"),
|
||||
verbose_name=_("access code"),
|
||||
null=True,
|
||||
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 "
|
||||
"viewing the organization."
|
||||
),
|
||||
)
|
||||
|
||||
def __contains__(self, item):
|
||||
if isinstance(item, int):
|
||||
|
@ -56,69 +93,128 @@ class Organization(models.Model):
|
|||
elif isinstance(item, Profile):
|
||||
return self.members.filter(id=item.id).exists()
|
||||
else:
|
||||
raise TypeError('Organization membership test must be Profile or primany key')
|
||||
raise TypeError(
|
||||
"Organization membership test must be Profile or primany key"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('organization_home', args=(self.id, self.slug))
|
||||
return reverse("organization_home", args=(self.id, self.slug))
|
||||
|
||||
def get_users_url(self):
|
||||
return reverse('organization_users', args=(self.id, self.slug))
|
||||
return reverse("organization_users", args=(self.id, self.slug))
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
ordering = ["name"]
|
||||
permissions = (
|
||||
('organization_admin', 'Administer organizations'),
|
||||
('edit_all_organization', 'Edit all organizations'),
|
||||
("organization_admin", "Administer organizations"),
|
||||
("edit_all_organization", "Edit all organizations"),
|
||||
)
|
||||
verbose_name = _('organization')
|
||||
verbose_name_plural = _('organizations')
|
||||
verbose_name = _("organization")
|
||||
verbose_name_plural = _("organizations")
|
||||
|
||||
|
||||
class Profile(models.Model):
|
||||
user = models.OneToOneField(User, verbose_name=_('user associated'), on_delete=models.CASCADE)
|
||||
about = models.TextField(verbose_name=_('self-description'), null=True, blank=True)
|
||||
timezone = models.CharField(max_length=50, verbose_name=_('location'), choices=TIMEZONE,
|
||||
default=settings.DEFAULT_USER_TIME_ZONE)
|
||||
language = models.ForeignKey('Language', verbose_name=_('preferred language'), on_delete=models.SET_DEFAULT,
|
||||
default=Language.get_default_language_pk)
|
||||
user = models.OneToOneField(
|
||||
User, verbose_name=_("user associated"), on_delete=models.CASCADE
|
||||
)
|
||||
about = models.TextField(verbose_name=_("self-description"), null=True, blank=True)
|
||||
timezone = models.CharField(
|
||||
max_length=50,
|
||||
verbose_name=_("location"),
|
||||
choices=TIMEZONE,
|
||||
default=settings.DEFAULT_USER_TIME_ZONE,
|
||||
)
|
||||
language = models.ForeignKey(
|
||||
"Language",
|
||||
verbose_name=_("preferred language"),
|
||||
on_delete=models.SET_DEFAULT,
|
||||
default=Language.get_default_language_pk,
|
||||
)
|
||||
points = models.FloatField(default=0, db_index=True)
|
||||
performance_points = models.FloatField(default=0, db_index=True)
|
||||
problem_count = models.IntegerField(default=0, db_index=True)
|
||||
ace_theme = models.CharField(max_length=30, choices=ACE_THEMES, default='github')
|
||||
last_access = models.DateTimeField(verbose_name=_('last access time'), default=now)
|
||||
ip = models.GenericIPAddressField(verbose_name=_('last IP'), blank=True, null=True)
|
||||
organizations = SortedManyToManyField(Organization, verbose_name=_('organization'), blank=True,
|
||||
related_name='members', related_query_name='member')
|
||||
display_rank = models.CharField(max_length=10, default='user', verbose_name=_('display rank'),
|
||||
choices=(('user', 'Normal User'), ('setter', 'Problem Setter'), ('admin', 'Admin')))
|
||||
mute = models.BooleanField(verbose_name=_('comment mute'), help_text=_('Some users are at their best when silent.'),
|
||||
default=False)
|
||||
is_unlisted = models.BooleanField(verbose_name=_('unlisted user'), help_text=_('User will not be ranked.'),
|
||||
default=False)
|
||||
ace_theme = models.CharField(max_length=30, choices=ACE_THEMES, default="github")
|
||||
last_access = models.DateTimeField(verbose_name=_("last access time"), default=now)
|
||||
ip = models.GenericIPAddressField(verbose_name=_("last IP"), blank=True, null=True)
|
||||
organizations = SortedManyToManyField(
|
||||
Organization,
|
||||
verbose_name=_("organization"),
|
||||
blank=True,
|
||||
related_name="members",
|
||||
related_query_name="member",
|
||||
)
|
||||
display_rank = models.CharField(
|
||||
max_length=10,
|
||||
default="user",
|
||||
verbose_name=_("display rank"),
|
||||
choices=(
|
||||
("user", "Normal User"),
|
||||
("setter", "Problem Setter"),
|
||||
("admin", "Admin"),
|
||||
),
|
||||
)
|
||||
mute = models.BooleanField(
|
||||
verbose_name=_("comment mute"),
|
||||
help_text=_("Some users are at their best when silent."),
|
||||
default=False,
|
||||
)
|
||||
is_unlisted = models.BooleanField(
|
||||
verbose_name=_("unlisted user"),
|
||||
help_text=_("User will not be ranked."),
|
||||
default=False,
|
||||
)
|
||||
is_banned_problem_voting = models.BooleanField(
|
||||
verbose_name=_('banned from voting'),
|
||||
verbose_name=_("banned from voting"),
|
||||
help_text=_("User will not be able to vote on problems' point values."),
|
||||
default=False,
|
||||
)
|
||||
rating = models.IntegerField(null=True, default=None)
|
||||
user_script = models.TextField(verbose_name=_('user script'), default='', blank=True, max_length=65536,
|
||||
help_text=_('User-defined JavaScript for site customization.'))
|
||||
current_contest = models.OneToOneField('ContestParticipation', verbose_name=_('current contest'),
|
||||
null=True, blank=True, related_name='+', on_delete=models.SET_NULL)
|
||||
math_engine = models.CharField(verbose_name=_('math engine'), choices=MATH_ENGINES_CHOICES, max_length=4,
|
||||
default=settings.MATHOID_DEFAULT_TYPE,
|
||||
help_text=_('the rendering engine used to render math'))
|
||||
is_totp_enabled = models.BooleanField(verbose_name=_('2FA enabled'), default=False,
|
||||
help_text=_('check to enable TOTP-based two factor authentication'))
|
||||
totp_key = EncryptedNullCharField(max_length=32, null=True, blank=True, verbose_name=_('TOTP key'),
|
||||
help_text=_('32 character base32-encoded key for TOTP'),
|
||||
validators=[RegexValidator('^$|^[A-Z2-7]{32}$',
|
||||
_('TOTP key must be empty or base32'))])
|
||||
notes = models.TextField(verbose_name=_('internal notes'), null=True, blank=True,
|
||||
help_text=_('Notes for administrators regarding this user.'))
|
||||
user_script = models.TextField(
|
||||
verbose_name=_("user script"),
|
||||
default="",
|
||||
blank=True,
|
||||
max_length=65536,
|
||||
help_text=_("User-defined JavaScript for site customization."),
|
||||
)
|
||||
current_contest = models.OneToOneField(
|
||||
"ContestParticipation",
|
||||
verbose_name=_("current contest"),
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="+",
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
math_engine = models.CharField(
|
||||
verbose_name=_("math engine"),
|
||||
choices=MATH_ENGINES_CHOICES,
|
||||
max_length=4,
|
||||
default=settings.MATHOID_DEFAULT_TYPE,
|
||||
help_text=_("the rendering engine used to render math"),
|
||||
)
|
||||
is_totp_enabled = models.BooleanField(
|
||||
verbose_name=_("2FA enabled"),
|
||||
default=False,
|
||||
help_text=_("check to enable TOTP-based two factor authentication"),
|
||||
)
|
||||
totp_key = EncryptedNullCharField(
|
||||
max_length=32,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("TOTP key"),
|
||||
help_text=_("32 character base32-encoded key for TOTP"),
|
||||
validators=[
|
||||
RegexValidator("^$|^[A-Z2-7]{32}$", _("TOTP key must be empty or base32"))
|
||||
],
|
||||
)
|
||||
notes = models.TextField(
|
||||
verbose_name=_("internal notes"),
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_("Notes for administrators regarding this user."),
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def organization(self):
|
||||
|
@ -133,33 +229,45 @@ class Profile(models.Model):
|
|||
@cached_property
|
||||
def count_unseen_notifications(self):
|
||||
query = {
|
||||
'read': False,
|
||||
"read": False,
|
||||
}
|
||||
return self.notifications.filter(**query).count()
|
||||
|
||||
|
||||
_pp_table = [pow(settings.DMOJ_PP_STEP, i) for i in range(settings.DMOJ_PP_ENTRIES)]
|
||||
|
||||
def calculate_points(self, table=_pp_table):
|
||||
from judge.models import Problem
|
||||
|
||||
public_problems = Problem.get_public_problems()
|
||||
data = (
|
||||
public_problems.filter(submission__user=self, submission__points__isnull=False)
|
||||
.annotate(max_points=Max('submission__points')).order_by('-max_points')
|
||||
.values_list('max_points', flat=True).filter(max_points__gt=0)
|
||||
public_problems.filter(
|
||||
submission__user=self, submission__points__isnull=False
|
||||
)
|
||||
.annotate(max_points=Max("submission__points"))
|
||||
.order_by("-max_points")
|
||||
.values_list("max_points", flat=True)
|
||||
.filter(max_points__gt=0)
|
||||
)
|
||||
extradata = (
|
||||
public_problems.filter(submission__user=self, submission__result='AC').values('id').distinct().count()
|
||||
public_problems.filter(submission__user=self, submission__result="AC")
|
||||
.values("id")
|
||||
.distinct()
|
||||
.count()
|
||||
)
|
||||
bonus_function = settings.DMOJ_PP_BONUS_FUNCTION
|
||||
points = sum(data)
|
||||
problems = len(data)
|
||||
entries = min(len(data), len(table))
|
||||
pp = sum(map(mul, table[:entries], data[:entries])) + bonus_function(extradata)
|
||||
if self.points != points or problems != self.problem_count or self.performance_points != pp:
|
||||
if (
|
||||
self.points != points
|
||||
or problems != self.problem_count
|
||||
or self.performance_points != pp
|
||||
):
|
||||
self.points = points
|
||||
self.problem_count = problems
|
||||
self.performance_points = pp
|
||||
self.save(update_fields=['points', 'problem_count', 'performance_points'])
|
||||
self.save(update_fields=["points", "problem_count", "performance_points"])
|
||||
return points
|
||||
|
||||
calculate_points.alters_data = True
|
||||
|
@ -172,9 +280,12 @@ class Profile(models.Model):
|
|||
|
||||
def update_contest(self):
|
||||
from judge.models import ContestParticipation
|
||||
|
||||
try:
|
||||
contest = self.current_contest
|
||||
if contest is not None and (contest.ended or not contest.contest.is_accessible_by(self.user)):
|
||||
if contest is not None and (
|
||||
contest.ended or not contest.contest.is_accessible_by(self.user)
|
||||
):
|
||||
self.remove_contest()
|
||||
except ContestParticipation.DoesNotExist:
|
||||
self.remove_contest()
|
||||
|
@ -182,86 +293,105 @@ class Profile(models.Model):
|
|||
update_contest.alters_data = True
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('user_page', args=(self.user.username,))
|
||||
return reverse("user_page", args=(self.user.username,))
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username
|
||||
|
||||
@classmethod
|
||||
def get_user_css_class(cls, display_rank, rating, rating_colors=settings.DMOJ_RATING_COLORS):
|
||||
def get_user_css_class(
|
||||
cls, display_rank, rating, rating_colors=settings.DMOJ_RATING_COLORS
|
||||
):
|
||||
if rating_colors:
|
||||
return 'rating %s %s' % (rating_class(rating) if rating is not None else 'rate-none', display_rank)
|
||||
return "rating %s %s" % (
|
||||
rating_class(rating) if rating is not None else "rate-none",
|
||||
display_rank,
|
||||
)
|
||||
return display_rank
|
||||
|
||||
@cached_property
|
||||
def css_class(self):
|
||||
return self.get_user_css_class(self.display_rank, self.rating)
|
||||
|
||||
def get_friends(self): #list of usernames, including you
|
||||
def get_friends(self): # list of usernames, including you
|
||||
friend_obj = self.following_users.all()
|
||||
ret = set()
|
||||
|
||||
if (friend_obj):
|
||||
if friend_obj:
|
||||
ret = set(friend.username for friend in friend_obj[0].users.all())
|
||||
|
||||
|
||||
ret.add(self.username)
|
||||
return ret
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('test_site', 'Shows in-progress development stuff'),
|
||||
('totp', 'Edit TOTP settings'),
|
||||
("test_site", "Shows in-progress development stuff"),
|
||||
("totp", "Edit TOTP settings"),
|
||||
)
|
||||
verbose_name = _('user profile')
|
||||
verbose_name_plural = _('user profiles')
|
||||
verbose_name = _("user profile")
|
||||
verbose_name_plural = _("user profiles")
|
||||
|
||||
|
||||
class OrganizationRequest(models.Model):
|
||||
user = models.ForeignKey(Profile, verbose_name=_('user'), related_name='requests', on_delete=models.CASCADE)
|
||||
organization = models.ForeignKey(Organization, verbose_name=_('organization'), related_name='requests',
|
||||
on_delete=models.CASCADE)
|
||||
time = models.DateTimeField(verbose_name=_('request time'), auto_now_add=True)
|
||||
state = models.CharField(max_length=1, verbose_name=_('state'), choices=(
|
||||
('P', 'Pending'),
|
||||
('A', 'Approved'),
|
||||
('R', 'Rejected'),
|
||||
))
|
||||
reason = models.TextField(verbose_name=_('reason'))
|
||||
user = models.ForeignKey(
|
||||
Profile,
|
||||
verbose_name=_("user"),
|
||||
related_name="requests",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
organization = models.ForeignKey(
|
||||
Organization,
|
||||
verbose_name=_("organization"),
|
||||
related_name="requests",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
time = models.DateTimeField(verbose_name=_("request time"), auto_now_add=True)
|
||||
state = models.CharField(
|
||||
max_length=1,
|
||||
verbose_name=_("state"),
|
||||
choices=(
|
||||
("P", "Pending"),
|
||||
("A", "Approved"),
|
||||
("R", "Rejected"),
|
||||
),
|
||||
)
|
||||
reason = models.TextField(verbose_name=_("reason"))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('organization join request')
|
||||
verbose_name_plural = _('organization join requests')
|
||||
verbose_name = _("organization join request")
|
||||
verbose_name_plural = _("organization join requests")
|
||||
|
||||
|
||||
class Friend(models.Model):
|
||||
users = models.ManyToManyField(Profile)
|
||||
current_user = models.ForeignKey(Profile, related_name="following_users", on_delete=CASCADE)
|
||||
current_user = models.ForeignKey(
|
||||
Profile, related_name="following_users", on_delete=CASCADE
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def is_friend(self, current_user, new_friend):
|
||||
try:
|
||||
return current_user.following_users.get().users \
|
||||
.filter(user=new_friend.user).exists()
|
||||
return (
|
||||
current_user.following_users.get()
|
||||
.users.filter(user=new_friend.user)
|
||||
.exists()
|
||||
)
|
||||
except:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def make_friend(self, current_user, new_friend):
|
||||
friend, created = self.objects.get_or_create(
|
||||
current_user = current_user
|
||||
)
|
||||
friend, created = self.objects.get_or_create(current_user=current_user)
|
||||
friend.users.add(new_friend)
|
||||
|
||||
@classmethod
|
||||
def remove_friend(self, current_user, new_friend):
|
||||
friend, created = self.objects.get_or_create(
|
||||
current_user = current_user
|
||||
)
|
||||
friend, created = self.objects.get_or_create(current_user=current_user)
|
||||
friend.users.remove(new_friend)
|
||||
|
||||
@classmethod
|
||||
def toggle_friend(self, current_user, new_friend):
|
||||
if (self.is_friend(current_user, new_friend)):
|
||||
if self.is_friend(current_user, new_friend):
|
||||
self.remove_friend(current_user, new_friend)
|
||||
else:
|
||||
self.make_friend(current_user, new_friend)
|
||||
|
|
|
@ -12,38 +12,82 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from judge.judgeapi import disconnect_judge
|
||||
|
||||
__all__ = ['Language', 'RuntimeVersion', 'Judge']
|
||||
__all__ = ["Language", "RuntimeVersion", "Judge"]
|
||||
|
||||
|
||||
class Language(models.Model):
|
||||
key = models.CharField(max_length=6, verbose_name=_('short identifier'),
|
||||
help_text=_('The identifier for this language; the same as its executor id for judges.'),
|
||||
unique=True)
|
||||
name = models.CharField(max_length=20, verbose_name=_('long name'),
|
||||
help_text=_('Longer name for the language, e.g. "Python 2" or "C++11".'))
|
||||
short_name = models.CharField(max_length=10, verbose_name=_('short name'),
|
||||
help_text=_('More readable, but short, name to display publicly; e.g. "PY2" or '
|
||||
'"C++11". If left blank, it will default to the '
|
||||
'short identifier.'),
|
||||
null=True, blank=True)
|
||||
common_name = models.CharField(max_length=10, verbose_name=_('common name'),
|
||||
help_text=_('Common name for the language. For example, the common name for C++03, '
|
||||
'C++11, and C++14 would be "C++"'))
|
||||
ace = models.CharField(max_length=20, verbose_name=_('ace mode name'),
|
||||
help_text=_('Language ID for Ace.js editor highlighting, appended to "mode-" to determine '
|
||||
'the Ace JavaScript file to use, e.g., "python".'))
|
||||
pygments = models.CharField(max_length=20, verbose_name=_('pygments name'),
|
||||
help_text=_('Language ID for Pygments highlighting in source windows.'))
|
||||
template = models.TextField(verbose_name=_('code template'),
|
||||
help_text=_('Code template to display in submission editor.'), blank=True)
|
||||
info = models.CharField(max_length=50, verbose_name=_('runtime info override'), blank=True,
|
||||
help_text=_("Do not set this unless you know what you're doing! It will override the "
|
||||
"usually more specific, judge-provided runtime info!"))
|
||||
description = models.TextField(verbose_name=_('language description'),
|
||||
help_text=_('Use this field to inform users of quirks with your environment, '
|
||||
'additional restrictions, etc.'), blank=True)
|
||||
extension = models.CharField(max_length=10, verbose_name=_('extension'),
|
||||
help_text=_('The extension of source files, e.g., "py" or "cpp".'))
|
||||
key = models.CharField(
|
||||
max_length=6,
|
||||
verbose_name=_("short identifier"),
|
||||
help_text=_(
|
||||
"The identifier for this language; the same as its executor id for judges."
|
||||
),
|
||||
unique=True,
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=20,
|
||||
verbose_name=_("long name"),
|
||||
help_text=_('Longer name for the language, e.g. "Python 2" or "C++11".'),
|
||||
)
|
||||
short_name = models.CharField(
|
||||
max_length=10,
|
||||
verbose_name=_("short name"),
|
||||
help_text=_(
|
||||
'More readable, but short, name to display publicly; e.g. "PY2" or '
|
||||
'"C++11". If left blank, it will default to the '
|
||||
"short identifier."
|
||||
),
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
common_name = models.CharField(
|
||||
max_length=10,
|
||||
verbose_name=_("common name"),
|
||||
help_text=_(
|
||||
"Common name for the language. For example, the common name for C++03, "
|
||||
'C++11, and C++14 would be "C++"'
|
||||
),
|
||||
)
|
||||
ace = models.CharField(
|
||||
max_length=20,
|
||||
verbose_name=_("ace mode name"),
|
||||
help_text=_(
|
||||
'Language ID for Ace.js editor highlighting, appended to "mode-" to determine '
|
||||
'the Ace JavaScript file to use, e.g., "python".'
|
||||
),
|
||||
)
|
||||
pygments = models.CharField(
|
||||
max_length=20,
|
||||
verbose_name=_("pygments name"),
|
||||
help_text=_("Language ID for Pygments highlighting in source windows."),
|
||||
)
|
||||
template = models.TextField(
|
||||
verbose_name=_("code template"),
|
||||
help_text=_("Code template to display in submission editor."),
|
||||
blank=True,
|
||||
)
|
||||
info = models.CharField(
|
||||
max_length=50,
|
||||
verbose_name=_("runtime info override"),
|
||||
blank=True,
|
||||
help_text=_(
|
||||
"Do not set this unless you know what you're doing! It will override the "
|
||||
"usually more specific, judge-provided runtime info!"
|
||||
),
|
||||
)
|
||||
description = models.TextField(
|
||||
verbose_name=_("language description"),
|
||||
help_text=_(
|
||||
"Use this field to inform users of quirks with your environment, "
|
||||
"additional restrictions, etc."
|
||||
),
|
||||
blank=True,
|
||||
)
|
||||
extension = models.CharField(
|
||||
max_length=10,
|
||||
verbose_name=_("extension"),
|
||||
help_text=_('The extension of source files, e.g., "py" or "cpp".'),
|
||||
)
|
||||
|
||||
def runtime_versions(self):
|
||||
runtimes = OrderedDict()
|
||||
|
@ -52,25 +96,29 @@ class Language(models.Model):
|
|||
id = runtime.name
|
||||
if id not in runtimes:
|
||||
runtimes[id] = set()
|
||||
if not runtime.version: # empty str == error determining version on judge side
|
||||
if (
|
||||
not runtime.version
|
||||
): # empty str == error determining version on judge side
|
||||
continue
|
||||
runtimes[id].add(runtime.version)
|
||||
|
||||
lang_versions = []
|
||||
for id, version_list in runtimes.items():
|
||||
lang_versions.append((id, sorted(version_list, key=lambda a: tuple(map(int, a.split('.'))))))
|
||||
lang_versions.append(
|
||||
(id, sorted(version_list, key=lambda a: tuple(map(int, a.split(".")))))
|
||||
)
|
||||
return lang_versions
|
||||
|
||||
@classmethod
|
||||
def get_common_name_map(cls):
|
||||
result = cache.get('lang:cn_map')
|
||||
result = cache.get("lang:cn_map")
|
||||
if result is not None:
|
||||
return result
|
||||
result = defaultdict(set)
|
||||
for id, cn in Language.objects.values_list('id', 'common_name'):
|
||||
for id, cn in Language.objects.values_list("id", "common_name"):
|
||||
result[cn].add(id)
|
||||
result = {id: cns for id, cns in result.items() if len(cns) > 1}
|
||||
cache.set('lang:cn_map', result, 86400)
|
||||
cache.set("lang:cn_map", result, 86400)
|
||||
return result
|
||||
|
||||
@cached_property
|
||||
|
@ -83,17 +131,19 @@ class Language(models.Model):
|
|||
@cached_property
|
||||
def display_name(self):
|
||||
if self.info:
|
||||
return '%s (%s)' % (self.name, self.info)
|
||||
return "%s (%s)" % (self.name, self.info)
|
||||
else:
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def get_python3(cls):
|
||||
# We really need a default language, and this app is in Python 3
|
||||
return Language.objects.get_or_create(key='PY3', defaults={'name': 'Python 3'})[0]
|
||||
return Language.objects.get_or_create(key="PY3", defaults={"name": "Python 3"})[
|
||||
0
|
||||
]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('runtime_list') + '#' + self.key
|
||||
return reverse("runtime_list") + "#" + self.key
|
||||
|
||||
@classmethod
|
||||
def get_default_language(cls):
|
||||
|
@ -107,36 +157,67 @@ class Language(models.Model):
|
|||
return cls.get_default_language().pk
|
||||
|
||||
class Meta:
|
||||
ordering = ['key']
|
||||
verbose_name = _('language')
|
||||
verbose_name_plural = _('languages')
|
||||
ordering = ["key"]
|
||||
verbose_name = _("language")
|
||||
verbose_name_plural = _("languages")
|
||||
|
||||
|
||||
class RuntimeVersion(models.Model):
|
||||
language = models.ForeignKey(Language, verbose_name=_('language to which this runtime belongs'), on_delete=CASCADE)
|
||||
judge = models.ForeignKey('Judge', verbose_name=_('judge on which this runtime exists'), on_delete=CASCADE)
|
||||
name = models.CharField(max_length=64, verbose_name=_('runtime name'))
|
||||
version = models.CharField(max_length=64, verbose_name=_('runtime version'), blank=True)
|
||||
priority = models.IntegerField(verbose_name=_('order in which to display this runtime'), default=0)
|
||||
language = models.ForeignKey(
|
||||
Language,
|
||||
verbose_name=_("language to which this runtime belongs"),
|
||||
on_delete=CASCADE,
|
||||
)
|
||||
judge = models.ForeignKey(
|
||||
"Judge", verbose_name=_("judge on which this runtime exists"), on_delete=CASCADE
|
||||
)
|
||||
name = models.CharField(max_length=64, verbose_name=_("runtime name"))
|
||||
version = models.CharField(
|
||||
max_length=64, verbose_name=_("runtime version"), blank=True
|
||||
)
|
||||
priority = models.IntegerField(
|
||||
verbose_name=_("order in which to display this runtime"), default=0
|
||||
)
|
||||
|
||||
|
||||
class Judge(models.Model):
|
||||
name = models.CharField(max_length=50, help_text=_('Server name, hostname-style'), unique=True)
|
||||
created = models.DateTimeField(auto_now_add=True, verbose_name=_('time of creation'))
|
||||
auth_key = models.CharField(max_length=100, help_text=_('A key to authenticate this judge'),
|
||||
verbose_name=_('authentication key'))
|
||||
is_blocked = models.BooleanField(verbose_name=_('block judge'), default=False,
|
||||
help_text=_('Whether this judge should be blocked from connecting, '
|
||||
'even if its key is correct.'))
|
||||
online = models.BooleanField(verbose_name=_('judge online status'), default=False)
|
||||
start_time = models.DateTimeField(verbose_name=_('judge start time'), null=True)
|
||||
ping = models.FloatField(verbose_name=_('response time'), null=True)
|
||||
load = models.FloatField(verbose_name=_('system load'), null=True,
|
||||
help_text=_('Load for the last minute, divided by processors to be fair.'))
|
||||
description = models.TextField(blank=True, verbose_name=_('description'))
|
||||
last_ip = models.GenericIPAddressField(verbose_name='Last connected IP', blank=True, null=True)
|
||||
problems = models.ManyToManyField('Problem', verbose_name=_('problems'), related_name='judges')
|
||||
runtimes = models.ManyToManyField(Language, verbose_name=_('judges'), related_name='judges')
|
||||
name = models.CharField(
|
||||
max_length=50, help_text=_("Server name, hostname-style"), unique=True
|
||||
)
|
||||
created = models.DateTimeField(
|
||||
auto_now_add=True, verbose_name=_("time of creation")
|
||||
)
|
||||
auth_key = models.CharField(
|
||||
max_length=100,
|
||||
help_text=_("A key to authenticate this judge"),
|
||||
verbose_name=_("authentication key"),
|
||||
)
|
||||
is_blocked = models.BooleanField(
|
||||
verbose_name=_("block judge"),
|
||||
default=False,
|
||||
help_text=_(
|
||||
"Whether this judge should be blocked from connecting, "
|
||||
"even if its key is correct."
|
||||
),
|
||||
)
|
||||
online = models.BooleanField(verbose_name=_("judge online status"), default=False)
|
||||
start_time = models.DateTimeField(verbose_name=_("judge start time"), null=True)
|
||||
ping = models.FloatField(verbose_name=_("response time"), null=True)
|
||||
load = models.FloatField(
|
||||
verbose_name=_("system load"),
|
||||
null=True,
|
||||
help_text=_("Load for the last minute, divided by processors to be fair."),
|
||||
)
|
||||
description = models.TextField(blank=True, verbose_name=_("description"))
|
||||
last_ip = models.GenericIPAddressField(
|
||||
verbose_name="Last connected IP", blank=True, null=True
|
||||
)
|
||||
problems = models.ManyToManyField(
|
||||
"Problem", verbose_name=_("problems"), related_name="judges"
|
||||
)
|
||||
runtimes = models.ManyToManyField(
|
||||
Language, verbose_name=_("judges"), related_name="judges"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
@ -148,22 +229,23 @@ class Judge(models.Model):
|
|||
|
||||
@cached_property
|
||||
def runtime_versions(self):
|
||||
qs = (self.runtimeversion_set.values('language__key', 'language__name', 'version', 'name')
|
||||
.order_by('language__key', 'priority'))
|
||||
qs = self.runtimeversion_set.values(
|
||||
"language__key", "language__name", "version", "name"
|
||||
).order_by("language__key", "priority")
|
||||
|
||||
ret = OrderedDict()
|
||||
|
||||
for data in qs:
|
||||
key = data['language__key']
|
||||
key = data["language__key"]
|
||||
if key not in ret:
|
||||
ret[key] = {'name': data['language__name'], 'runtime': []}
|
||||
ret[key]['runtime'].append((data['name'], (data['version'],)))
|
||||
ret[key] = {"name": data["language__name"], "runtime": []}
|
||||
ret[key]["runtime"].append((data["name"], (data["version"],)))
|
||||
|
||||
return list(ret.items())
|
||||
|
||||
@cached_property
|
||||
def uptime(self):
|
||||
return timezone.now() - self.start_time if self.online else 'N/A'
|
||||
return timezone.now() - self.start_time if self.online else "N/A"
|
||||
|
||||
@cached_property
|
||||
def ping_ms(self):
|
||||
|
@ -171,9 +253,9 @@ class Judge(models.Model):
|
|||
|
||||
@cached_property
|
||||
def runtime_list(self):
|
||||
return map(attrgetter('name'), self.runtimes.all())
|
||||
return map(attrgetter("name"), self.runtimes.all())
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
verbose_name = _('judge')
|
||||
verbose_name_plural = _('judges')
|
||||
ordering = ["name"]
|
||||
verbose_name = _("judge")
|
||||
verbose_name_plural = _("judges")
|
||||
|
|
|
@ -14,92 +14,130 @@ from judge.models.profile import Profile
|
|||
from judge.models.runtime import Language
|
||||
from judge.utils.unicode import utf8bytes
|
||||
|
||||
__all__ = ['SUBMISSION_RESULT', 'Submission', 'SubmissionSource', 'SubmissionTestCase']
|
||||
__all__ = ["SUBMISSION_RESULT", "Submission", "SubmissionSource", "SubmissionTestCase"]
|
||||
|
||||
SUBMISSION_RESULT = (
|
||||
('AC', _('Accepted')),
|
||||
('WA', _('Wrong Answer')),
|
||||
('TLE', _('Time Limit Exceeded')),
|
||||
('MLE', _('Memory Limit Exceeded')),
|
||||
('OLE', _('Output Limit Exceeded')),
|
||||
('IR', _('Invalid Return')),
|
||||
('RTE', _('Runtime Error')),
|
||||
('CE', _('Compile Error')),
|
||||
('IE', _('Internal Error')),
|
||||
('SC', _('Short circuit')),
|
||||
('AB', _('Aborted')),
|
||||
("AC", _("Accepted")),
|
||||
("WA", _("Wrong Answer")),
|
||||
("TLE", _("Time Limit Exceeded")),
|
||||
("MLE", _("Memory Limit Exceeded")),
|
||||
("OLE", _("Output Limit Exceeded")),
|
||||
("IR", _("Invalid Return")),
|
||||
("RTE", _("Runtime Error")),
|
||||
("CE", _("Compile Error")),
|
||||
("IE", _("Internal Error")),
|
||||
("SC", _("Short circuit")),
|
||||
("AB", _("Aborted")),
|
||||
)
|
||||
|
||||
|
||||
class Submission(models.Model):
|
||||
STATUS = (
|
||||
('QU', _('Queued')),
|
||||
('P', _('Processing')),
|
||||
('G', _('Grading')),
|
||||
('D', _('Completed')),
|
||||
('IE', _('Internal Error')),
|
||||
('CE', _('Compile Error')),
|
||||
('AB', _('Aborted')),
|
||||
("QU", _("Queued")),
|
||||
("P", _("Processing")),
|
||||
("G", _("Grading")),
|
||||
("D", _("Completed")),
|
||||
("IE", _("Internal Error")),
|
||||
("CE", _("Compile Error")),
|
||||
("AB", _("Aborted")),
|
||||
)
|
||||
IN_PROGRESS_GRADING_STATUS = ('QU', 'P', 'G')
|
||||
IN_PROGRESS_GRADING_STATUS = ("QU", "P", "G")
|
||||
RESULT = SUBMISSION_RESULT
|
||||
USER_DISPLAY_CODES = {
|
||||
'AC': _('Accepted'),
|
||||
'WA': _('Wrong Answer'),
|
||||
'SC': "Short Circuited",
|
||||
'TLE': _('Time Limit Exceeded'),
|
||||
'MLE': _('Memory Limit Exceeded'),
|
||||
'OLE': _('Output Limit Exceeded'),
|
||||
'IR': _('Invalid Return'),
|
||||
'RTE': _('Runtime Error'),
|
||||
'CE': _('Compile Error'),
|
||||
'IE': _('Internal Error (judging server error)'),
|
||||
'QU': _('Queued'),
|
||||
'P': _('Processing'),
|
||||
'G': _('Grading'),
|
||||
'D': _('Completed'),
|
||||
'AB': _('Aborted'),
|
||||
"AC": _("Accepted"),
|
||||
"WA": _("Wrong Answer"),
|
||||
"SC": "Short Circuited",
|
||||
"TLE": _("Time Limit Exceeded"),
|
||||
"MLE": _("Memory Limit Exceeded"),
|
||||
"OLE": _("Output Limit Exceeded"),
|
||||
"IR": _("Invalid Return"),
|
||||
"RTE": _("Runtime Error"),
|
||||
"CE": _("Compile Error"),
|
||||
"IE": _("Internal Error (judging server error)"),
|
||||
"QU": _("Queued"),
|
||||
"P": _("Processing"),
|
||||
"G": _("Grading"),
|
||||
"D": _("Completed"),
|
||||
"AB": _("Aborted"),
|
||||
}
|
||||
|
||||
user = models.ForeignKey(Profile, on_delete=models.CASCADE)
|
||||
problem = models.ForeignKey(Problem, on_delete=models.CASCADE)
|
||||
date = models.DateTimeField(verbose_name=_('submission time'), auto_now_add=True, db_index=True)
|
||||
time = models.FloatField(verbose_name=_('execution time'), null=True, db_index=True)
|
||||
memory = models.FloatField(verbose_name=_('memory usage'), null=True)
|
||||
points = models.FloatField(verbose_name=_('points granted'), null=True, db_index=True)
|
||||
language = models.ForeignKey(Language, verbose_name=_('submission language'), on_delete=models.CASCADE)
|
||||
status = models.CharField(verbose_name=_('status'), max_length=2, choices=STATUS, default='QU', db_index=True)
|
||||
result = models.CharField(verbose_name=_('result'), max_length=3, choices=SUBMISSION_RESULT,
|
||||
default=None, null=True, blank=True, db_index=True)
|
||||
error = models.TextField(verbose_name=_('compile errors'), null=True, blank=True)
|
||||
date = models.DateTimeField(
|
||||
verbose_name=_("submission time"), auto_now_add=True, db_index=True
|
||||
)
|
||||
time = models.FloatField(verbose_name=_("execution time"), null=True, db_index=True)
|
||||
memory = models.FloatField(verbose_name=_("memory usage"), null=True)
|
||||
points = models.FloatField(
|
||||
verbose_name=_("points granted"), null=True, db_index=True
|
||||
)
|
||||
language = models.ForeignKey(
|
||||
Language, verbose_name=_("submission language"), on_delete=models.CASCADE
|
||||
)
|
||||
status = models.CharField(
|
||||
verbose_name=_("status"),
|
||||
max_length=2,
|
||||
choices=STATUS,
|
||||
default="QU",
|
||||
db_index=True,
|
||||
)
|
||||
result = models.CharField(
|
||||
verbose_name=_("result"),
|
||||
max_length=3,
|
||||
choices=SUBMISSION_RESULT,
|
||||
default=None,
|
||||
null=True,
|
||||
blank=True,
|
||||
db_index=True,
|
||||
)
|
||||
error = models.TextField(verbose_name=_("compile errors"), null=True, blank=True)
|
||||
current_testcase = models.IntegerField(default=0)
|
||||
batch = models.BooleanField(verbose_name=_('batched cases'), default=False)
|
||||
case_points = models.FloatField(verbose_name=_('test case points'), default=0)
|
||||
case_total = models.FloatField(verbose_name=_('test case total points'), default=0)
|
||||
judged_on = models.ForeignKey('Judge', verbose_name=_('judged on'), null=True, blank=True,
|
||||
on_delete=models.SET_NULL)
|
||||
judged_date = models.DateTimeField(verbose_name=_('submission judge time'), default=None, null=True)
|
||||
was_rejudged = models.BooleanField(verbose_name=_('was rejudged by admin'), default=False)
|
||||
is_pretested = models.BooleanField(verbose_name=_('was ran on pretests only'), default=False)
|
||||
contest_object = models.ForeignKey('Contest', verbose_name=_('contest'), null=True, blank=True,
|
||||
on_delete=models.SET_NULL, related_name='+')
|
||||
batch = models.BooleanField(verbose_name=_("batched cases"), default=False)
|
||||
case_points = models.FloatField(verbose_name=_("test case points"), default=0)
|
||||
case_total = models.FloatField(verbose_name=_("test case total points"), default=0)
|
||||
judged_on = models.ForeignKey(
|
||||
"Judge",
|
||||
verbose_name=_("judged on"),
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
judged_date = models.DateTimeField(
|
||||
verbose_name=_("submission judge time"), default=None, null=True
|
||||
)
|
||||
was_rejudged = models.BooleanField(
|
||||
verbose_name=_("was rejudged by admin"), default=False
|
||||
)
|
||||
is_pretested = models.BooleanField(
|
||||
verbose_name=_("was ran on pretests only"), default=False
|
||||
)
|
||||
contest_object = models.ForeignKey(
|
||||
"Contest",
|
||||
verbose_name=_("contest"),
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="+",
|
||||
)
|
||||
|
||||
objects = TranslatedProblemForeignKeyQuerySet.as_manager()
|
||||
|
||||
@classmethod
|
||||
def result_class_from_code(cls, result, case_points, case_total):
|
||||
if result == 'AC':
|
||||
if result == "AC":
|
||||
if case_points == case_total:
|
||||
return 'AC'
|
||||
return '_AC'
|
||||
return "AC"
|
||||
return "_AC"
|
||||
return result
|
||||
|
||||
@property
|
||||
def result_class(self):
|
||||
# This exists to save all these conditionals from being executed (slowly) in each row.jade template
|
||||
if self.status in ('IE', 'CE'):
|
||||
if self.status in ("IE", "CE"):
|
||||
return self.status
|
||||
return Submission.result_class_from_code(self.result, self.case_points, self.case_total)
|
||||
return Submission.result_class_from_code(
|
||||
self.result, self.case_points, self.case_total
|
||||
)
|
||||
|
||||
@property
|
||||
def memory_bytes(self):
|
||||
|
@ -111,12 +149,11 @@ class Submission(models.Model):
|
|||
|
||||
@property
|
||||
def long_status(self):
|
||||
return Submission.USER_DISPLAY_CODES.get(self.short_status, '')
|
||||
return Submission.USER_DISPLAY_CODES.get(self.short_status, "")
|
||||
|
||||
def judge(self, *args, **kwargs):
|
||||
judge_submission(self, *args, **kwargs)
|
||||
|
||||
|
||||
judge.alters_data = True
|
||||
|
||||
def abort(self):
|
||||
|
@ -131,8 +168,12 @@ class Submission(models.Model):
|
|||
return
|
||||
|
||||
contest_problem = contest.problem
|
||||
contest.points = round(self.case_points / self.case_total * contest_problem.points
|
||||
if self.case_total > 0 else 0, 3)
|
||||
contest.points = round(
|
||||
self.case_points / self.case_total * contest_problem.points
|
||||
if self.case_total > 0
|
||||
else 0,
|
||||
3,
|
||||
)
|
||||
if not contest_problem.partial and contest.points != contest_problem.points:
|
||||
contest.points = 0
|
||||
contest.save()
|
||||
|
@ -142,18 +183,22 @@ class Submission(models.Model):
|
|||
|
||||
@property
|
||||
def is_graded(self):
|
||||
return self.status not in ('QU', 'P', 'G')
|
||||
return self.status not in ("QU", "P", "G")
|
||||
|
||||
@cached_property
|
||||
def contest_key(self):
|
||||
if hasattr(self, 'contest'):
|
||||
if hasattr(self, "contest"):
|
||||
return self.contest_object.key
|
||||
|
||||
def __str__(self):
|
||||
return 'Submission %d of %s by %s' % (self.id, self.problem, self.user.user.username)
|
||||
return "Submission %d of %s by %s" % (
|
||||
self.id,
|
||||
self.problem,
|
||||
self.user.user.username,
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('submission_status', args=(self.id,))
|
||||
return reverse("submission_status", args=(self.id,))
|
||||
|
||||
@cached_property
|
||||
def contest_or_none(self):
|
||||
|
@ -164,8 +209,14 @@ class Submission(models.Model):
|
|||
|
||||
@classmethod
|
||||
def get_id_secret(cls, sub_id):
|
||||
return (hmac.new(utf8bytes(settings.EVENT_DAEMON_SUBMISSION_KEY), b'%d' % sub_id, hashlib.sha512)
|
||||
.hexdigest()[:16] + '%08x' % sub_id)
|
||||
return (
|
||||
hmac.new(
|
||||
utf8bytes(settings.EVENT_DAEMON_SUBMISSION_KEY),
|
||||
b"%d" % sub_id,
|
||||
hashlib.sha512,
|
||||
).hexdigest()[:16]
|
||||
+ "%08x" % sub_id
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def id_secret(self):
|
||||
|
@ -173,47 +224,61 @@ class Submission(models.Model):
|
|||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('abort_any_submission', 'Abort any submission'),
|
||||
('rejudge_submission', 'Rejudge the submission'),
|
||||
('rejudge_submission_lot', 'Rejudge a lot of submissions'),
|
||||
('spam_submission', 'Submit without limit'),
|
||||
('view_all_submission', 'View all submission'),
|
||||
('resubmit_other', "Resubmit others' submission"),
|
||||
("abort_any_submission", "Abort any submission"),
|
||||
("rejudge_submission", "Rejudge the submission"),
|
||||
("rejudge_submission_lot", "Rejudge a lot of submissions"),
|
||||
("spam_submission", "Submit without limit"),
|
||||
("view_all_submission", "View all submission"),
|
||||
("resubmit_other", "Resubmit others' submission"),
|
||||
)
|
||||
verbose_name = _('submission')
|
||||
verbose_name_plural = _('submissions')
|
||||
verbose_name = _("submission")
|
||||
verbose_name_plural = _("submissions")
|
||||
|
||||
|
||||
class SubmissionSource(models.Model):
|
||||
submission = models.OneToOneField(Submission, on_delete=models.CASCADE, verbose_name=_('associated submission'),
|
||||
related_name='source')
|
||||
source = models.TextField(verbose_name=_('source code'), max_length=65536)
|
||||
submission = models.OneToOneField(
|
||||
Submission,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("associated submission"),
|
||||
related_name="source",
|
||||
)
|
||||
source = models.TextField(verbose_name=_("source code"), max_length=65536)
|
||||
|
||||
def __str__(self):
|
||||
return 'Source of %s' % self.submission
|
||||
return "Source of %s" % self.submission
|
||||
|
||||
|
||||
class SubmissionTestCase(models.Model):
|
||||
RESULT = SUBMISSION_RESULT
|
||||
|
||||
submission = models.ForeignKey(Submission, verbose_name=_('associated submission'),
|
||||
related_name='test_cases', on_delete=models.CASCADE)
|
||||
case = models.IntegerField(verbose_name=_('test case ID'))
|
||||
status = models.CharField(max_length=3, verbose_name=_('status flag'), choices=SUBMISSION_RESULT)
|
||||
time = models.FloatField(verbose_name=_('execution time'), null=True)
|
||||
memory = models.FloatField(verbose_name=_('memory usage'), null=True)
|
||||
points = models.FloatField(verbose_name=_('points granted'), null=True)
|
||||
total = models.FloatField(verbose_name=_('points possible'), null=True)
|
||||
batch = models.IntegerField(verbose_name=_('batch number'), null=True)
|
||||
feedback = models.CharField(max_length=50, verbose_name=_('judging feedback'), blank=True)
|
||||
extended_feedback = models.TextField(verbose_name=_('extended judging feedback'), blank=True)
|
||||
output = models.TextField(verbose_name=_('program output'), blank=True)
|
||||
submission = models.ForeignKey(
|
||||
Submission,
|
||||
verbose_name=_("associated submission"),
|
||||
related_name="test_cases",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
case = models.IntegerField(verbose_name=_("test case ID"))
|
||||
status = models.CharField(
|
||||
max_length=3, verbose_name=_("status flag"), choices=SUBMISSION_RESULT
|
||||
)
|
||||
time = models.FloatField(verbose_name=_("execution time"), null=True)
|
||||
memory = models.FloatField(verbose_name=_("memory usage"), null=True)
|
||||
points = models.FloatField(verbose_name=_("points granted"), null=True)
|
||||
total = models.FloatField(verbose_name=_("points possible"), null=True)
|
||||
batch = models.IntegerField(verbose_name=_("batch number"), null=True)
|
||||
feedback = models.CharField(
|
||||
max_length=50, verbose_name=_("judging feedback"), blank=True
|
||||
)
|
||||
extended_feedback = models.TextField(
|
||||
verbose_name=_("extended judging feedback"), blank=True
|
||||
)
|
||||
output = models.TextField(verbose_name=_("program output"), blank=True)
|
||||
|
||||
@property
|
||||
def long_status(self):
|
||||
return Submission.USER_DISPLAY_CODES.get(self.status, '')
|
||||
return Submission.USER_DISPLAY_CODES.get(self.status, "")
|
||||
|
||||
class Meta:
|
||||
unique_together = ('submission', 'case')
|
||||
verbose_name = _('submission test case')
|
||||
verbose_name_plural = _('submission test cases')
|
||||
unique_together = ("submission", "case")
|
||||
verbose_name = _("submission test case")
|
||||
verbose_name_plural = _("submission test cases")
|
||||
|
|
|
@ -7,24 +7,43 @@ from judge.models.profile import Profile
|
|||
|
||||
|
||||
class Ticket(models.Model):
|
||||
title = models.CharField(max_length=100, verbose_name=_('ticket title'))
|
||||
user = models.ForeignKey(Profile, verbose_name=_('ticket creator'), related_name='tickets',
|
||||
on_delete=models.CASCADE)
|
||||
time = models.DateTimeField(verbose_name=_('creation time'), auto_now_add=True)
|
||||
assignees = models.ManyToManyField(Profile, verbose_name=_('assignees'), related_name='assigned_tickets')
|
||||
notes = models.TextField(verbose_name=_('quick notes'), blank=True,
|
||||
help_text=_('Staff notes for this issue to aid in processing.'))
|
||||
content_type = models.ForeignKey(ContentType, verbose_name=_('linked item type'),
|
||||
on_delete=models.CASCADE)
|
||||
object_id = models.PositiveIntegerField(verbose_name=_('linked item ID'))
|
||||
title = models.CharField(max_length=100, verbose_name=_("ticket title"))
|
||||
user = models.ForeignKey(
|
||||
Profile,
|
||||
verbose_name=_("ticket creator"),
|
||||
related_name="tickets",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
time = models.DateTimeField(verbose_name=_("creation time"), auto_now_add=True)
|
||||
assignees = models.ManyToManyField(
|
||||
Profile, verbose_name=_("assignees"), related_name="assigned_tickets"
|
||||
)
|
||||
notes = models.TextField(
|
||||
verbose_name=_("quick notes"),
|
||||
blank=True,
|
||||
help_text=_("Staff notes for this issue to aid in processing."),
|
||||
)
|
||||
content_type = models.ForeignKey(
|
||||
ContentType, verbose_name=_("linked item type"), on_delete=models.CASCADE
|
||||
)
|
||||
object_id = models.PositiveIntegerField(verbose_name=_("linked item ID"))
|
||||
linked_item = GenericForeignKey()
|
||||
is_open = models.BooleanField(verbose_name=_('is ticket open?'), default=True)
|
||||
is_open = models.BooleanField(verbose_name=_("is ticket open?"), default=True)
|
||||
|
||||
|
||||
class TicketMessage(models.Model):
|
||||
ticket = models.ForeignKey(Ticket, verbose_name=_('ticket'), related_name='messages',
|
||||
related_query_name='message', on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(Profile, verbose_name=_('poster'), related_name='ticket_messages',
|
||||
on_delete=models.CASCADE)
|
||||
body = models.TextField(verbose_name=_('message body'))
|
||||
time = models.DateTimeField(verbose_name=_('message time'), auto_now_add=True)
|
||||
ticket = models.ForeignKey(
|
||||
Ticket,
|
||||
verbose_name=_("ticket"),
|
||||
related_name="messages",
|
||||
related_query_name="message",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
Profile,
|
||||
verbose_name=_("poster"),
|
||||
related_name="ticket_messages",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
body = models.TextField(verbose_name=_("message body"))
|
||||
time = models.DateTimeField(verbose_name=_("message time"), auto_now_add=True)
|
||||
|
|
|
@ -4,25 +4,36 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from judge.models import Profile, Problem, ProblemType
|
||||
|
||||
__all__ = ['VolunteerProblemVote']
|
||||
__all__ = ["VolunteerProblemVote"]
|
||||
|
||||
|
||||
class VolunteerProblemVote(models.Model):
|
||||
voter = models.ForeignKey(Profile, related_name='volunteer_problem_votes', on_delete=CASCADE)
|
||||
problem = models.ForeignKey(Problem, related_name='volunteer_user_votes', on_delete=CASCADE)
|
||||
voter = models.ForeignKey(
|
||||
Profile, related_name="volunteer_problem_votes", on_delete=CASCADE
|
||||
)
|
||||
problem = models.ForeignKey(
|
||||
Problem, related_name="volunteer_user_votes", on_delete=CASCADE
|
||||
)
|
||||
time = models.DateTimeField(auto_now_add=True)
|
||||
knowledge_points = models.PositiveIntegerField(verbose_name=_('knowledge points'),
|
||||
help_text=_('Points awarded by knowledge difficulty'))
|
||||
thinking_points = models.PositiveIntegerField(verbose_name=_('thinking points'),
|
||||
help_text=_('Points awarded by thinking difficulty'))
|
||||
types = models.ManyToManyField(ProblemType, verbose_name=_('problem types'),
|
||||
help_text=_('The type of problem, '
|
||||
"as shown on the problem's page."))
|
||||
feedback = models.TextField(verbose_name=_('feedback'), blank=True)
|
||||
knowledge_points = models.PositiveIntegerField(
|
||||
verbose_name=_("knowledge points"),
|
||||
help_text=_("Points awarded by knowledge difficulty"),
|
||||
)
|
||||
thinking_points = models.PositiveIntegerField(
|
||||
verbose_name=_("thinking points"),
|
||||
help_text=_("Points awarded by thinking difficulty"),
|
||||
)
|
||||
types = models.ManyToManyField(
|
||||
ProblemType,
|
||||
verbose_name=_("problem types"),
|
||||
help_text=_("The type of problem, " "as shown on the problem's page."),
|
||||
)
|
||||
feedback = models.TextField(verbose_name=_("feedback"), blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('volunteer vote')
|
||||
verbose_name_plural = _('volunteer votes')
|
||||
unique_together = ['voter', 'problem']
|
||||
verbose_name = _("volunteer vote")
|
||||
verbose_name_plural = _("volunteer votes")
|
||||
unique_together = ["voter", "problem"]
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.voter} for {self.problem.code}'
|
||||
return f"{self.voter} for {self.problem.code}"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue