Cloned DMOJ
This commit is contained in:
parent
f623974b58
commit
49dc9ff10c
513 changed files with 132349 additions and 39 deletions
29
judge/models/__init__.py
Normal file
29
judge/models/__init__.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
from reversion import revisions
|
||||
|
||||
from judge.models.choices import ACE_THEMES, EFFECTIVE_MATH_ENGINES, MATH_ENGINES_CHOICES, TIMEZONE
|
||||
from judge.models.comment import Comment, CommentLock, CommentVote
|
||||
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
|
||||
from judge.models.problem_data import CHECKERS, ProblemData, ProblemTestCase, problem_data_storage, \
|
||||
problem_directory_file
|
||||
from judge.models.profile import Organization, OrganizationRequest, Profile
|
||||
from judge.models.runtime import Judge, Language, RuntimeVersion
|
||||
from judge.models.submission import SUBMISSION_RESULT, Submission, SubmissionSource, SubmissionTestCase
|
||||
from judge.models.ticket import Ticket, TicketMessage
|
||||
|
||||
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(ContestProblem)
|
||||
revisions.register(Organization)
|
||||
revisions.register(BlogPost)
|
||||
revisions.register(Solution)
|
||||
revisions.register(Judge, fields=['name', 'created', 'auth_key', 'description'])
|
||||
revisions.register(Language)
|
||||
revisions.register(Comment, fields=['author', 'time', 'page', 'score', 'body', 'hidden', 'parent'])
|
||||
del revisions
|
66
judge/models/choices.py
Normal file
66
judge/models/choices.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
from collections import defaultdict
|
||||
from operator import itemgetter
|
||||
|
||||
import pytz
|
||||
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)
|
||||
else:
|
||||
area, loc = 'Other', tz
|
||||
if not loc.startswith('GMT'):
|
||||
data[area].append((tz, loc))
|
||||
return sorted(data.items(), key=itemgetter(0))
|
||||
|
||||
|
||||
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'),
|
||||
)
|
||||
|
||||
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')),
|
||||
)
|
||||
|
||||
EFFECTIVE_MATH_ENGINES = ('svg', 'mml', 'tex', 'jax')
|
185
judge/models/comment.py
Normal file
185
judge/models/comment.py
Normal file
|
@ -0,0 +1,185 @@
|
|||
import itertools
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import CASCADE
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from mptt.fields import TreeForeignKey
|
||||
from mptt.models import MPTTModel
|
||||
from reversion.models import Version
|
||||
|
||||
from judge.models.contest import Contest
|
||||
from judge.models.interface import BlogPost
|
||||
from judge.models.problem import Problem
|
||||
from judge.models.profile import Profile
|
||||
from judge.utils.cachedict import CacheDict
|
||||
|
||||
__all__ = ['Comment', 'CommentLock', 'CommentVote']
|
||||
|
||||
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')
|
||||
|
||||
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')
|
||||
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)
|
||||
versions = VersionRelation()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('comment')
|
||||
verbose_name_plural = _('comments')
|
||||
|
||||
class MPTTMeta:
|
||||
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')
|
||||
|
||||
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 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]
|
||||
if not slice:
|
||||
break
|
||||
for comment in slice:
|
||||
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:'):
|
||||
try:
|
||||
if contest_access[comment.page[2:]]:
|
||||
output.append(comment)
|
||||
except Contest.DoesNotExist:
|
||||
pass
|
||||
elif comment.page.startswith('b:'):
|
||||
try:
|
||||
if blog_access[comment.page[2:]]:
|
||||
output.append(comment)
|
||||
except BlogPost.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
output.append(comment)
|
||||
if len(output) >= n:
|
||||
return output
|
||||
return output
|
||||
|
||||
@cached_property
|
||||
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:]
|
||||
slug = cache.get(key)
|
||||
if slug is None:
|
||||
try:
|
||||
slug = BlogPost.objects.get(id=self.page[2:]).slug
|
||||
except ObjectDoesNotExist:
|
||||
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:],))
|
||||
except Exception:
|
||||
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>'
|
||||
except ObjectDoesNotExist:
|
||||
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)
|
||||
|
||||
def __str__(self):
|
||||
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)))
|
||||
# It's rather stupid to put a query specific property on the model, but the alternative requires
|
||||
# digging Django internals, and could not be guaranteed to work forever.
|
||||
# Hence it is left here for when the alternative breaks.
|
||||
# @property
|
||||
# def vote_score(self):
|
||||
# queryset = self.votes.all()
|
||||
# if not queryset:
|
||||
# return 0
|
||||
# return queryset[0].score
|
||||
|
||||
|
||||
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)
|
||||
score = models.IntegerField()
|
||||
|
||||
class Meta:
|
||||
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])
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('override_comment_lock', _('Override comment lock')),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.page)
|
412
judge/models/contest.py
Normal file
412
judge/models/contest.py
Normal file
|
@ -0,0 +1,412 @@
|
|||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MinValueValidator, RegexValidator
|
||||
from django.db import models, transaction
|
||||
from django.db.models import CASCADE
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from jsonfield import JSONField
|
||||
from moss import MOSS_LANG_C, MOSS_LANG_CC, MOSS_LANG_JAVA, MOSS_LANG_PYTHON
|
||||
|
||||
from judge import contest_format
|
||||
from judge.models.problem import Problem
|
||||
from judge.models.profile import Organization, Profile
|
||||
from judge.models.submission import Submission
|
||||
from judge.ratings import rate_contest
|
||||
|
||||
__all__ = ['Contest', 'ContestTag', 'ContestParticipation', 'ContestProblem', 'ContestSubmission', 'Rating']
|
||||
|
||||
|
||||
class ContestTag(models.Model):
|
||||
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)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('contest_tag', args=[self.name])
|
||||
|
||||
@property
|
||||
def text_color(self, cache={}):
|
||||
if self.color not in cache:
|
||||
if len(self.color) == 4:
|
||||
r, g, b = [ord(bytes.fromhex(i * 2)) for i in self.color[1:]]
|
||||
else:
|
||||
r, g, b = [i for i in bytes.fromhex(self.color[1:])]
|
||||
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')
|
||||
|
||||
|
||||
class Contest(models.Model):
|
||||
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)
|
||||
organizers = models.ManyToManyField(Profile, help_text=_('These people will be able to edit the contest.'),
|
||||
related_name='organizers+')
|
||||
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)
|
||||
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)
|
||||
hide_scoreboard = models.BooleanField(verbose_name=_('hide scoreboard'),
|
||||
help_text=_('Whether the scoreboard should remain hidden for the duration '
|
||||
'of the contest.'),
|
||||
default=False)
|
||||
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=False)
|
||||
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.'))
|
||||
|
||||
@cached_property
|
||||
def format_class(self):
|
||||
return contest_format.formats[self.format_name]
|
||||
|
||||
@cached_property
|
||||
def format(self):
|
||||
return self.format_class(self, self.format_config)
|
||||
|
||||
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?')
|
||||
self.format_class.validate(self.format_config)
|
||||
|
||||
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 False
|
||||
|
||||
def can_see_scoreboard(self, user):
|
||||
if user.has_perm('judge.see_private_contest'):
|
||||
return True
|
||||
if user.is_authenticated and self.organizers.filter(id=user.profile.id).exists():
|
||||
return True
|
||||
if not self.is_visible:
|
||||
return False
|
||||
if self.start_time is not None and self.start_time > timezone.now():
|
||||
return False
|
||||
if self.hide_scoreboard and not self.is_in_contest(user) and self.end_time > timezone.now():
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def contest_window_length(self):
|
||||
return self.end_time - self.start_time
|
||||
|
||||
@cached_property
|
||||
def _now(self):
|
||||
# This ensures that all methods talk about the same now.
|
||||
return timezone.now()
|
||||
|
||||
@cached_property
|
||||
def can_join(self):
|
||||
return self.start_time <= self._now
|
||||
|
||||
@property
|
||||
def time_before_start(self):
|
||||
if self.start_time >= self._now:
|
||||
return self.start_time - self._now
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def time_before_end(self):
|
||||
if self.end_time >= self._now:
|
||||
return self.end_time - self._now
|
||||
else:
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
def ended(self):
|
||||
return self.end_time < self._now
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('contest_view', args=(self.key,))
|
||||
|
||||
def update_user_count(self):
|
||||
self.user_count = self.users.filter(virtual=0).count()
|
||||
self.save()
|
||||
|
||||
update_user_count.alters_data = True
|
||||
|
||||
@cached_property
|
||||
def show_scoreboard(self):
|
||||
if self.hide_scoreboard and not self.ended:
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_accessible_by(self, user):
|
||||
# Contest is publicly visible
|
||||
if self.is_visible:
|
||||
# Contest is not private
|
||||
if not self.is_private and not self.is_organization_private:
|
||||
return True
|
||||
if user.is_authenticated:
|
||||
# User is in the organizations it is private to
|
||||
if self.organizations.filter(id__in=user.profile.organizations.all()).exists():
|
||||
return True
|
||||
# User is in the group of private contestants
|
||||
if self.private_contestants.filter(id=user.profile.id).exists():
|
||||
return True
|
||||
|
||||
# If the user can view all contests
|
||||
if user.has_perm('judge.see_private_contest'):
|
||||
return True
|
||||
|
||||
# User can edit the contest
|
||||
return self.is_editable_by(user)
|
||||
|
||||
def is_editable_by(self, user):
|
||||
# If the user can edit all contests
|
||||
if user.has_perm('judge.edit_all_contest'):
|
||||
return True
|
||||
|
||||
# If the user is a contest organizer
|
||||
if user.has_perm('judge.edit_own_contest') and \
|
||||
self.organizers.filter(id=user.profile.id).exists():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def rate(self):
|
||||
Rating.objects.filter(contest__end_time__gte=self.end_time).delete()
|
||||
for contest in Contest.objects.filter(is_rated=True, end_time__gte=self.end_time).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')),
|
||||
)
|
||||
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.IntegerField(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.'))
|
||||
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'])
|
||||
recompute_results.alters_data = True
|
||||
|
||||
def set_disqualified(self, disqualified):
|
||||
self.is_disqualified = disqualified
|
||||
self.recompute_results()
|
||||
if self.contest.is_rated and self.contest.ratings.exists():
|
||||
self.contest.rate()
|
||||
if self.is_disqualified:
|
||||
if self.user.current_contest == self:
|
||||
self.user.remove_contest()
|
||||
self.contest.banned_users.add(self.user)
|
||||
else:
|
||||
self.contest.banned_users.remove(self.user)
|
||||
set_disqualified.alters_data = True
|
||||
|
||||
@property
|
||||
def live(self):
|
||||
return self.virtual == self.LIVE
|
||||
|
||||
@property
|
||||
def spectate(self):
|
||||
return self.virtual == self.SPECTATE
|
||||
|
||||
@cached_property
|
||||
def start(self):
|
||||
contest = self.contest
|
||||
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):
|
||||
contest = self.contest
|
||||
if self.spectate:
|
||||
return contest.end_time
|
||||
if self.virtual:
|
||||
if contest.time_limit:
|
||||
return self.real_start + contest.time_limit
|
||||
else:
|
||||
return self.real_start + (contest.end_time - contest.start_time)
|
||||
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):
|
||||
# This ensures that all methods talk about the same now.
|
||||
return timezone.now()
|
||||
|
||||
@property
|
||||
def ended(self):
|
||||
return self.end_time is not None and self.end_time < self._now
|
||||
|
||||
@property
|
||||
def time_remaining(self):
|
||||
end = self.end_time
|
||||
if end is not None and end >= self._now:
|
||||
return end - self._now
|
||||
|
||||
def __str__(self):
|
||||
if self.spectate:
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('contest participation')
|
||||
verbose_name_plural = _('contest participations')
|
||||
|
||||
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(verbose_name=_('output prefix length override'), null=True, blank=True)
|
||||
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')
|
||||
|
||||
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
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'))
|
||||
volatility = models.IntegerField(verbose_name=_('volatility'))
|
||||
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')
|
||||
|
||||
|
||||
class ContestMoss(models.Model):
|
||||
LANG_MAPPING = [
|
||||
('C', MOSS_LANG_C),
|
||||
('C++', MOSS_LANG_CC),
|
||||
('Java', MOSS_LANG_JAVA),
|
||||
('Python', MOSS_LANG_PYTHON),
|
||||
]
|
||||
|
||||
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')
|
94
judge/models/interface.py
Normal file
94
judge/models/interface.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
import re
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from mptt.fields import TreeForeignKey
|
||||
from mptt.models import MPTTModel
|
||||
|
||||
from judge.models.profile import Profile
|
||||
|
||||
__all__ = ['MiscConfig', 'validate_regex', 'NavigationBar', 'BlogPost']
|
||||
|
||||
|
||||
class MiscConfig(models.Model):
|
||||
key = models.CharField(max_length=30, db_index=True)
|
||||
value = models.TextField(blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.key
|
||||
|
||||
class Meta:
|
||||
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)
|
||||
|
||||
|
||||
class NavigationBar(MPTTModel):
|
||||
class Meta:
|
||||
verbose_name = _('navigation item')
|
||||
verbose_name_plural = _('navigation bar')
|
||||
|
||||
class MPTTMeta:
|
||||
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)
|
||||
|
||||
def __str__(self):
|
||||
return self.label
|
||||
|
||||
@property
|
||||
def pattern(self, cache={}):
|
||||
# A cache with a bad policy is an alias for memory leak
|
||||
# Thankfully, there will never be too many regexes to cache.
|
||||
if self.regex in cache:
|
||||
return cache[self.regex]
|
||||
else:
|
||||
pattern = cache[self.regex] = re.compile(self.regex, re.VERBOSE)
|
||||
return pattern
|
||||
|
||||
|
||||
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)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('blog_post', args=(self.id, self.slug))
|
||||
|
||||
def can_see(self, user):
|
||||
if self.visible and self.publish_on <= timezone.now():
|
||||
return True
|
||||
if user.has_perm('judge.edit_all_post'):
|
||||
return True
|
||||
return user.is_authenticated 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')
|
20
judge/models/message.py
Normal file
20
judge/models/message.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
from django.db import models
|
||||
from django.db.models import CASCADE
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from judge.models.profile import Profile
|
||||
|
||||
__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)
|
||||
|
||||
|
||||
class PrivateMessageThread(models.Model):
|
||||
messages = models.ManyToManyField(PrivateMessage, verbose_name=_('messages in the thread'))
|
413
judge/models/problem.py
Normal file
413
judge/models/problem.py
Normal file
|
@ -0,0 +1,413 @@
|
|||
from operator import attrgetter
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.cache import cache
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import CASCADE, F, QuerySet, SET_NULL
|
||||
from django.db.models.expressions import RawSQL
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from judge.fulltext import SearchQuerySet
|
||||
from judge.models.profile import Organization, Profile
|
||||
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']
|
||||
|
||||
|
||||
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'))
|
||||
|
||||
def __str__(self):
|
||||
return self.full_name
|
||||
|
||||
class Meta:
|
||||
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'))
|
||||
|
||||
def __str__(self):
|
||||
return self.full_name
|
||||
|
||||
class Meta:
|
||||
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'))
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('license', args=(self.key,))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('license')
|
||||
verbose_name_plural = _('licenses')
|
||||
|
||||
|
||||
class TranslatedProblemQuerySet(SearchQuerySet):
|
||||
def __init__(self, **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()))
|
||||
|
||||
|
||||
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)
|
||||
# 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())}
|
||||
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)])
|
||||
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)
|
||||
|
||||
objects = TranslatedProblemQuerySet.as_manager()
|
||||
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)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Problem, self).__init__(*args, **kwargs)
|
||||
self._translated_name_cache = {}
|
||||
self._i18n_name = None
|
||||
self.__original_code = self.code
|
||||
|
||||
@cached_property
|
||||
def types_list(self):
|
||||
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')
|
||||
|
||||
def is_editor(self, profile):
|
||||
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:
|
||||
return True
|
||||
return user.has_perm('judge.edit_own_problem') and self.is_editor(user.profile)
|
||||
|
||||
def is_accessible_by(self, user):
|
||||
# Problem is public.
|
||||
if self.is_public:
|
||||
# Problem is not private to an organization.
|
||||
if not self.is_organization_private:
|
||||
return True
|
||||
|
||||
# If the user can see all organization private problems.
|
||||
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()):
|
||||
return True
|
||||
|
||||
# If the user can view all problems.
|
||||
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):
|
||||
return True
|
||||
|
||||
# If user is a tester.
|
||||
if self.testers.filter(id=user.profile.id).exists():
|
||||
return True
|
||||
|
||||
# If user is currently in a contest containing that problem.
|
||||
current = user.profile.current_contest_id
|
||||
if current is None:
|
||||
return False
|
||||
from judge.models import ContestProblem
|
||||
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)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('problem_detail', args=(self.code,))
|
||||
|
||||
@cached_property
|
||||
def author_ids(self):
|
||||
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)
|
||||
|
||||
@cached_property
|
||||
def tester_ids(self):
|
||||
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))
|
||||
|
||||
@property
|
||||
def usable_languages(self):
|
||||
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]
|
||||
except IndexError:
|
||||
name = self.name
|
||||
self._translated_name_cache[language] = name
|
||||
return name
|
||||
|
||||
@property
|
||||
def i18n_name(self):
|
||||
if self._i18n_name is None:
|
||||
self._i18n_name = self._trans[0].name if self._trans else self.name
|
||||
return self._i18n_name
|
||||
|
||||
@i18n_name.setter
|
||||
def i18n_name(self, value):
|
||||
self._i18n_name = value
|
||||
|
||||
@property
|
||||
def clarifications(self):
|
||||
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()
|
||||
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
|
||||
else:
|
||||
self.ac_rate = 0
|
||||
self.save()
|
||||
|
||||
update_stats.alters_data = True
|
||||
|
||||
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}
|
||||
limit_ids = set(limits.keys())
|
||||
common = []
|
||||
|
||||
for cn, ids in Language.get_common_name_map().items():
|
||||
if ids - limit_ids:
|
||||
continue
|
||||
limit = set(limits[id][1] for id in ids)
|
||||
if len(limit) == 1:
|
||||
limit = next(iter(limit))
|
||||
common.append((cn, limit))
|
||||
for id in ids:
|
||||
del limits[id]
|
||||
|
||||
limits = list(limits.values()) + common
|
||||
limits.sort()
|
||||
return limits
|
||||
|
||||
@property
|
||||
def language_time_limit(self):
|
||||
key = 'problem_tls:%d' % self.id
|
||||
result = cache.get(key)
|
||||
if result is not None:
|
||||
return result
|
||||
result = self._get_limits('time_limit')
|
||||
cache.set(key, result)
|
||||
return result
|
||||
|
||||
@property
|
||||
def language_memory_limit(self):
|
||||
key = 'problem_mls:%d' % self.id
|
||||
result = cache.get(key)
|
||||
if result is not None:
|
||||
return result
|
||||
result = self._get_limits('memory_limit')
|
||||
cache.set(key, result)
|
||||
return result
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super(Problem, self).save(*args, **kwargs)
|
||||
if self.code != self.__original_code:
|
||||
try:
|
||||
problem_data = self.data_files
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
problem_data._update_code(self.__original_code, self.code)
|
||||
|
||||
save.alters_data = True
|
||||
|
||||
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'),
|
||||
)
|
||||
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'))
|
||||
|
||||
class Meta:
|
||||
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)
|
||||
|
||||
|
||||
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)])
|
||||
|
||||
class Meta:
|
||||
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'))
|
||||
|
||||
def get_absolute_url(self):
|
||||
problem = self.problem
|
||||
if problem is None:
|
||||
return reverse('home')
|
||||
else:
|
||||
return reverse('problem_editorial', args=[problem.code])
|
||||
|
||||
def __str__(self):
|
||||
return _('Editorial for %s') % self.problem.name
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('see_private_solution', 'See hidden solutions'),
|
||||
)
|
||||
verbose_name = _('solution')
|
||||
verbose_name_plural = _('solutions')
|
94
judge/models/problem_data.py
Normal file
94
judge/models/problem_data.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
import errno
|
||||
import os
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from judge.utils.problem_data import ProblemDataStorage
|
||||
|
||||
__all__ = ['problem_data_storage', 'problem_directory_file', 'ProblemData', 'ProblemTestCase', 'CHECKERS']
|
||||
|
||||
problem_data_storage = ProblemDataStorage()
|
||||
|
||||
|
||||
def _problem_directory_file(code, filename):
|
||||
return os.path.join(code, os.path.basename(filename))
|
||||
|
||||
|
||||
def problem_directory_file(data, filename):
|
||||
return _problem_directory_file(data.problem.code, 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')),
|
||||
)
|
||||
|
||||
|
||||
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'))
|
||||
|
||||
__original_zipfile = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ProblemData, self).__init__(*args, **kwargs)
|
||||
self.__original_zipfile = self.zipfile
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.zipfile != self.__original_zipfile:
|
||||
self.__original_zipfile.delete(save=False)
|
||||
return super(ProblemData, self).save(*args, **kwargs)
|
||||
|
||||
def has_yml(self):
|
||||
return problem_data_storage.exists('%s/init.yml' % self.problem.code)
|
||||
|
||||
def _update_code(self, original, new):
|
||||
try:
|
||||
problem_data_storage.rename(original, new)
|
||||
except OSError as e:
|
||||
if e.errno != errno.ENOENT:
|
||||
raise
|
||||
if self.zipfile:
|
||||
self.zipfile.name = _problem_directory_file(new, self.zipfile.name)
|
||||
if self.generator:
|
||||
self.generator.name = _problem_directory_file(new, self.generator.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'))
|
204
judge/models/profile.py
Normal file
204
judge/models/profile.py
Normal file
|
@ -0,0 +1,204 @@
|
|||
from operator import mul
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import Max
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from fernet_fields import EncryptedCharField
|
||||
from sortedm2m.fields import SortedManyToManyField
|
||||
|
||||
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']
|
||||
|
||||
|
||||
class EncryptedNullCharField(EncryptedCharField):
|
||||
def get_prep_value(self, value):
|
||||
if not value:
|
||||
return None
|
||||
return super(EncryptedNullCharField, self).get_prep_value(value)
|
||||
|
||||
|
||||
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.'))
|
||||
|
||||
def __contains__(self, item):
|
||||
if isinstance(item, int):
|
||||
return self.members.filter(id=item).exists()
|
||||
elif isinstance(item, Profile):
|
||||
return self.members.filter(id=item.id).exists()
|
||||
else:
|
||||
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))
|
||||
|
||||
def get_users_url(self):
|
||||
return reverse('organization_users', args=(self.id, self.slug))
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
permissions = (
|
||||
('organization_admin', 'Administer organizations'),
|
||||
('edit_all_organization', 'Edit all 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)
|
||||
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)
|
||||
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.'))
|
||||
|
||||
@cached_property
|
||||
def organization(self):
|
||||
# We do this to take advantage of prefetch_related
|
||||
orgs = self.organizations.all()
|
||||
return orgs[0] if orgs else None
|
||||
|
||||
@cached_property
|
||||
def username(self):
|
||||
return self.user.username
|
||||
|
||||
_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
|
||||
data = (Problem.objects.filter(submission__user=self, submission__points__isnull=False, is_public=True,
|
||||
is_organization_private=False)
|
||||
.annotate(max_points=Max('submission__points')).order_by('-max_points')
|
||||
.values_list('max_points', flat=True).filter(max_points__gt=0))
|
||||
extradata = Problem.objects.filter(submission__user=self, submission__result='AC', is_public=True) \
|
||||
.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:
|
||||
self.points = points
|
||||
self.problem_count = problems
|
||||
self.performance_points = pp
|
||||
self.save(update_fields=['points', 'problem_count', 'performance_points'])
|
||||
return points
|
||||
|
||||
calculate_points.alters_data = True
|
||||
|
||||
def remove_contest(self):
|
||||
self.current_contest = None
|
||||
self.save()
|
||||
|
||||
remove_contest.alters_data = True
|
||||
|
||||
def update_contest(self):
|
||||
contest = self.current_contest
|
||||
if contest is not None and (contest.ended or not contest.contest.is_accessible_by(self.user)):
|
||||
self.remove_contest()
|
||||
|
||||
update_contest.alters_data = True
|
||||
|
||||
def get_absolute_url(self):
|
||||
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):
|
||||
if rating_colors:
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('test_site', 'Shows in-progress development stuff'),
|
||||
('totp', 'Edit TOTP settings'),
|
||||
)
|
||||
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'))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('organization join request')
|
||||
verbose_name_plural = _('organization join requests')
|
176
judge/models/runtime.py
Normal file
176
judge/models/runtime.py
Normal file
|
@ -0,0 +1,176 @@
|
|||
from collections import OrderedDict, defaultdict
|
||||
from operator import attrgetter
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.db.models import CASCADE
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from judge.judgeapi import disconnect_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".'))
|
||||
|
||||
def runtime_versions(self):
|
||||
runtimes = OrderedDict()
|
||||
# There be dragons here if two judges specify different priorities
|
||||
for runtime in self.runtimeversion_set.all():
|
||||
id = runtime.name
|
||||
if id not in runtimes:
|
||||
runtimes[id] = set()
|
||||
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('.'))))))
|
||||
return lang_versions
|
||||
|
||||
@classmethod
|
||||
def get_common_name_map(cls):
|
||||
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'):
|
||||
result[cn].add(id)
|
||||
result = {id: cns for id, cns in result.items() if len(cns) > 1}
|
||||
cache.set('lang:cn_map', result, 86400)
|
||||
return result
|
||||
|
||||
@cached_property
|
||||
def short_display_name(self):
|
||||
return self.short_name or self.key
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@cached_property
|
||||
def display_name(self):
|
||||
if 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]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('runtime_list') + '#' + self.key
|
||||
|
||||
@classmethod
|
||||
def get_default_language(cls):
|
||||
return Language.objects.get(key=settings.DEFAULT_USER_LANGUAGE)
|
||||
|
||||
@classmethod
|
||||
def get_default_language_pk(cls):
|
||||
return cls.get_default_language().pk
|
||||
|
||||
class Meta:
|
||||
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)
|
||||
|
||||
|
||||
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')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def disconnect(self, force=False):
|
||||
disconnect_judge(self, force=force)
|
||||
|
||||
disconnect.alters_data = True
|
||||
|
||||
@cached_property
|
||||
def runtime_versions(self):
|
||||
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']
|
||||
if key not in ret:
|
||||
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'
|
||||
|
||||
@cached_property
|
||||
def ping_ms(self):
|
||||
return self.ping * 1000 if self.ping is not None else None
|
||||
|
||||
@cached_property
|
||||
def runtime_list(self):
|
||||
return map(attrgetter('name'), self.runtimes.all())
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
verbose_name = _('judge')
|
||||
verbose_name_plural = _('judges')
|
217
judge/models/submission.py
Normal file
217
judge/models/submission.py
Normal file
|
@ -0,0 +1,217 @@
|
|||
import hashlib
|
||||
import hmac
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from judge.judgeapi import abort_submission, judge_submission
|
||||
from judge.models.problem import Problem, TranslatedProblemForeignKeyQuerySet
|
||||
from judge.models.profile import Profile
|
||||
from judge.models.runtime import Language
|
||||
from judge.utils.unicode import utf8bytes
|
||||
|
||||
__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')),
|
||||
)
|
||||
|
||||
|
||||
class Submission(models.Model):
|
||||
STATUS = (
|
||||
('QU', _('Queued')),
|
||||
('P', _('Processing')),
|
||||
('G', _('Grading')),
|
||||
('D', _('Completed')),
|
||||
('IE', _('Internal Error')),
|
||||
('CE', _('Compile Error')),
|
||||
('AB', _('Aborted')),
|
||||
)
|
||||
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'),
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
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 case_points == case_total:
|
||||
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'):
|
||||
return self.status
|
||||
return Submission.result_class_from_code(self.result, self.case_points, self.case_total)
|
||||
|
||||
@property
|
||||
def memory_bytes(self):
|
||||
return self.memory * 1024 if self.memory is not None else 0
|
||||
|
||||
@property
|
||||
def short_status(self):
|
||||
return self.result or self.status
|
||||
|
||||
@property
|
||||
def long_status(self):
|
||||
return Submission.USER_DISPLAY_CODES.get(self.short_status, '')
|
||||
|
||||
def judge(self, rejudge=False, batch_rejudge=False):
|
||||
judge_submission(self, rejudge, batch_rejudge)
|
||||
|
||||
judge.alters_data = True
|
||||
|
||||
def abort(self):
|
||||
abort_submission(self)
|
||||
|
||||
abort.alters_data = True
|
||||
|
||||
def update_contest(self):
|
||||
try:
|
||||
contest = self.contest
|
||||
except AttributeError:
|
||||
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)
|
||||
if not contest_problem.partial and contest.points != contest_problem.points:
|
||||
contest.points = 0
|
||||
contest.save()
|
||||
contest.participation.recompute_results()
|
||||
|
||||
update_contest.alters_data = True
|
||||
|
||||
@property
|
||||
def is_graded(self):
|
||||
return self.status not in ('QU', 'P', 'G')
|
||||
|
||||
@cached_property
|
||||
def contest_key(self):
|
||||
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)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('submission_status', args=(self.id,))
|
||||
|
||||
@cached_property
|
||||
def contest_or_none(self):
|
||||
try:
|
||||
return self.contest
|
||||
except ObjectDoesNotExist:
|
||||
return None
|
||||
|
||||
@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)
|
||||
|
||||
@cached_property
|
||||
def id_secret(self):
|
||||
return self.get_id_secret(self.id)
|
||||
|
||||
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"),
|
||||
)
|
||||
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)
|
||||
|
||||
def __str__(self):
|
||||
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)
|
||||
|
||||
@property
|
||||
def long_status(self):
|
||||
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')
|
30
judge/models/ticket.py
Normal file
30
judge/models/ticket.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
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'))
|
||||
linked_item = GenericForeignKey()
|
||||
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)
|
Loading…
Add table
Add a link
Reference in a new issue