Cloned DMOJ

This commit is contained in:
thanhluong 2020-01-21 15:35:58 +09:00
parent f623974b58
commit 49dc9ff10c
513 changed files with 132349 additions and 39 deletions

29
judge/models/__init__.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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')

View 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
View 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
View 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
View 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
View 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)