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

1
judge/__init__.py Normal file
View file

@ -0,0 +1 @@
default_app_config = 'judge.apps.JudgeAppConfig'

37
judge/admin/__init__.py Normal file
View file

@ -0,0 +1,37 @@
from django.contrib import admin
from django.contrib.admin.models import LogEntry
from judge.admin.comments import CommentAdmin
from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestTagAdmin
from judge.admin.interface import BlogPostAdmin, LicenseAdmin, LogEntryAdmin, NavigationBarAdmin
from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin
from judge.admin.problem import ProblemAdmin
from judge.admin.profile import ProfileAdmin
from judge.admin.runtime import JudgeAdmin, LanguageAdmin
from judge.admin.submission import SubmissionAdmin
from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin
from judge.admin.ticket import TicketAdmin
from judge.models import BlogPost, Comment, CommentLock, Contest, ContestParticipation, \
ContestTag, Judge, Language, License, MiscConfig, NavigationBar, Organization, \
OrganizationRequest, Problem, ProblemGroup, ProblemType, Profile, Submission, Ticket
admin.site.register(BlogPost, BlogPostAdmin)
admin.site.register(Comment, CommentAdmin)
admin.site.register(CommentLock)
admin.site.register(Contest, ContestAdmin)
admin.site.register(ContestParticipation, ContestParticipationAdmin)
admin.site.register(ContestTag, ContestTagAdmin)
admin.site.register(Judge, JudgeAdmin)
admin.site.register(Language, LanguageAdmin)
admin.site.register(License, LicenseAdmin)
admin.site.register(LogEntry, LogEntryAdmin)
admin.site.register(MiscConfig)
admin.site.register(NavigationBar, NavigationBarAdmin)
admin.site.register(Organization, OrganizationAdmin)
admin.site.register(OrganizationRequest, OrganizationRequestAdmin)
admin.site.register(Problem, ProblemAdmin)
admin.site.register(ProblemGroup, ProblemGroupAdmin)
admin.site.register(ProblemType, ProblemTypeAdmin)
admin.site.register(Profile, ProfileAdmin)
admin.site.register(Submission, SubmissionAdmin)
admin.site.register(Ticket, TicketAdmin)

64
judge/admin/comments.py Normal file
View file

@ -0,0 +1,64 @@
from django.forms import ModelForm
from django.urls import reverse_lazy
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _, ungettext
from reversion.admin import VersionAdmin
from judge.models import Comment
from judge.widgets import AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
class CommentForm(ModelForm):
class Meta:
widgets = {
'author': AdminHeavySelect2Widget(data_view='profile_select2'),
'parent': AdminHeavySelect2Widget(data_view='comment_select2'),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets['body'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('comment_preview'))
class CommentAdmin(VersionAdmin):
fieldsets = (
(None, {'fields': ('author', 'page', 'parent', 'score', 'hidden')}),
('Content', {'fields': ('body',)}),
)
list_display = ['author', 'linked_page', 'time']
search_fields = ['author__user__username', 'page', 'body']
actions = ['hide_comment', 'unhide_comment']
list_filter = ['hidden']
actions_on_top = True
actions_on_bottom = True
form = CommentForm
date_hierarchy = 'time'
def get_queryset(self, request):
return Comment.objects.order_by('-time')
def hide_comment(self, request, queryset):
count = queryset.update(hidden=True)
self.message_user(request, ungettext('%d comment successfully hidden.',
'%d comments successfully hidden.',
count) % count)
hide_comment.short_description = _('Hide comments')
def unhide_comment(self, request, queryset):
count = queryset.update(hidden=False)
self.message_user(request, ungettext('%d comment successfully unhidden.',
'%d comments successfully unhidden.',
count) % count)
unhide_comment.short_description = _('Unhide comments')
def linked_page(self, obj):
link = obj.link
if link is not None:
return format_html('<a href="{0}">{1}</a>', link, obj.page)
else:
return format_html('{0}', obj.page)
linked_page.short_description = _('Associated page')
linked_page.admin_order_field = 'page'
def save_model(self, request, obj, form, change):
super(CommentAdmin, self).save_model(request, obj, form, change)
if obj.hidden:
obj.get_descendants().update(hidden=obj.hidden)

269
judge/admin/contest.py Normal file
View file

@ -0,0 +1,269 @@
from django.conf.urls import url
from django.contrib import admin
from django.core.exceptions import PermissionDenied
from django.db import connection, transaction
from django.db.models import Q, TextField
from django.forms import ModelForm, ModelMultipleChoiceField
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _, ungettext
from reversion.admin import VersionAdmin
from judge.models import Contest, ContestProblem, ContestSubmission, Profile, Rating
from judge.ratings import rate_contest
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, AdminPagedownWidget, \
AdminSelect2MultipleWidget, AdminSelect2Widget, HeavyPreviewAdminPageDownWidget
class AdminHeavySelect2Widget(AdminHeavySelect2Widget):
@property
def is_hidden(self):
return False
class ContestTagForm(ModelForm):
contests = ModelMultipleChoiceField(
label=_('Included contests'),
queryset=Contest.objects.all(),
required=False,
widget=AdminHeavySelect2MultipleWidget(data_view='contest_select2'))
class ContestTagAdmin(admin.ModelAdmin):
fields = ('name', 'color', 'description', 'contests')
list_display = ('name', 'color')
actions_on_top = True
actions_on_bottom = True
form = ContestTagForm
if AdminPagedownWidget is not None:
formfield_overrides = {
TextField: {'widget': AdminPagedownWidget},
}
def save_model(self, request, obj, form, change):
super(ContestTagAdmin, self).save_model(request, obj, form, change)
obj.contests.set(form.cleaned_data['contests'])
def get_form(self, request, obj=None, **kwargs):
form = super(ContestTagAdmin, self).get_form(request, obj, **kwargs)
if obj is not None:
form.base_fields['contests'].initial = obj.contests.all()
return form
class ContestProblemInlineForm(ModelForm):
class Meta:
widgets = {'problem': AdminHeavySelect2Widget(data_view='problem_select2')}
class ContestProblemInline(admin.TabularInline):
model = ContestProblem
verbose_name = _('Problem')
verbose_name_plural = 'Problems'
fields = ('problem', 'points', 'partial', 'is_pretested', 'max_submissions', 'output_prefix_override', 'order',
'rejudge_column')
readonly_fields = ('rejudge_column',)
form = ContestProblemInlineForm
def rejudge_column(self, obj):
if obj.id is None:
return ''
return format_html('<a class="button rejudge-link" href="{}">Rejudge</a>',
reverse('admin:judge_contest_rejudge', args=(obj.contest.id, obj.id)))
rejudge_column.short_description = ''
class ContestForm(ModelForm):
def __init__(self, *args, **kwargs):
super(ContestForm, self).__init__(*args, **kwargs)
if 'rate_exclude' in self.fields:
if self.instance and self.instance.id:
self.fields['rate_exclude'].queryset = \
Profile.objects.filter(contest_history__contest=self.instance).distinct()
else:
self.fields['rate_exclude'].queryset = Profile.objects.none()
self.fields['banned_users'].widget.can_add_related = False
def clean(self):
cleaned_data = super(ContestForm, self).clean()
cleaned_data['banned_users'].filter(current_contest__contest=self.instance).update(current_contest=None)
class Meta:
widgets = {
'organizers': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
'private_contestants': AdminHeavySelect2MultipleWidget(data_view='profile_select2',
attrs={'style': 'width: 100%'}),
'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2'),
'tags': AdminSelect2MultipleWidget,
'banned_users': AdminHeavySelect2MultipleWidget(data_view='profile_select2',
attrs={'style': 'width: 100%'}),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets['description'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('contest_preview'))
class ContestAdmin(VersionAdmin):
fieldsets = (
(None, {'fields': ('key', 'name', 'organizers')}),
(_('Settings'), {'fields': ('is_visible', 'use_clarifications', 'hide_problem_tags', 'hide_scoreboard',
'run_pretests_only')}),
(_('Scheduling'), {'fields': ('start_time', 'end_time', 'time_limit')}),
(_('Details'), {'fields': ('description', 'og_image', 'logo_override_image', 'tags', 'summary')}),
(_('Format'), {'fields': ('format_name', 'format_config')}),
(_('Rating'), {'fields': ('is_rated', 'rate_all', 'rating_floor', 'rating_ceiling', 'rate_exclude')}),
(_('Access'), {'fields': ('access_code', 'is_private', 'private_contestants', 'is_organization_private',
'organizations')}),
(_('Justice'), {'fields': ('banned_users',)}),
)
list_display = ('key', 'name', 'is_visible', 'is_rated', 'start_time', 'end_time', 'time_limit', 'user_count')
actions = ['make_visible', 'make_hidden']
inlines = [ContestProblemInline]
actions_on_top = True
actions_on_bottom = True
form = ContestForm
change_list_template = 'admin/judge/contest/change_list.html'
filter_horizontal = ['rate_exclude']
date_hierarchy = 'start_time'
def get_queryset(self, request):
queryset = Contest.objects.all()
if request.user.has_perm('judge.edit_all_contest'):
return queryset
else:
return queryset.filter(organizers__id=request.profile.id)
def get_readonly_fields(self, request, obj=None):
readonly = []
if not request.user.has_perm('judge.contest_rating'):
readonly += ['is_rated', 'rate_all', 'rate_exclude']
if not request.user.has_perm('judge.contest_access_code'):
readonly += ['access_code']
if not request.user.has_perm('judge.create_private_contest'):
readonly += ['is_private', 'private_contestants', 'is_organization_private', 'organizations']
return readonly
def has_change_permission(self, request, obj=None):
if not request.user.has_perm('judge.edit_own_contest'):
return False
if request.user.has_perm('judge.edit_all_contest') or obj is None:
return True
return obj.organizers.filter(id=request.profile.id).exists()
def make_visible(self, request, queryset):
count = queryset.update(is_visible=True)
self.message_user(request, ungettext('%d contest successfully marked as visible.',
'%d contests successfully marked as visible.',
count) % count)
make_visible.short_description = _('Mark contests as visible')
def make_hidden(self, request, queryset):
count = queryset.update(is_visible=False)
self.message_user(request, ungettext('%d contest successfully marked as hidden.',
'%d contests successfully marked as hidden.',
count) % count)
make_hidden.short_description = _('Mark contests as hidden')
def get_urls(self):
return [
url(r'^rate/all/$', self.rate_all_view, name='judge_contest_rate_all'),
url(r'^(\d+)/rate/$', self.rate_view, name='judge_contest_rate'),
url(r'^(\d+)/judge/(\d+)/$', self.rejudge_view, name='judge_contest_rejudge'),
] + super(ContestAdmin, self).get_urls()
def rejudge_view(self, request, contest_id, problem_id):
queryset = ContestSubmission.objects.filter(problem_id=problem_id).select_related('submission')
for model in queryset:
model.submission.judge(rejudge=True)
self.message_user(request, ungettext('%d submission was successfully scheduled for rejudging.',
'%d submissions were successfully scheduled for rejudging.',
len(queryset)) % len(queryset))
return HttpResponseRedirect(reverse('admin:judge_contest_change', args=(contest_id,)))
def rate_all_view(self, request):
if not request.user.has_perm('judge.contest_rating'):
raise PermissionDenied()
with transaction.atomic():
if connection.vendor == 'sqlite':
Rating.objects.all().delete()
else:
cursor = connection.cursor()
cursor.execute('TRUNCATE TABLE `%s`' % Rating._meta.db_table)
cursor.close()
Profile.objects.update(rating=None)
for contest in Contest.objects.filter(is_rated=True).order_by('end_time'):
rate_contest(contest)
return HttpResponseRedirect(reverse('admin:judge_contest_changelist'))
def rate_view(self, request, id):
if not request.user.has_perm('judge.contest_rating'):
raise PermissionDenied()
contest = get_object_or_404(Contest, id=id)
if not contest.is_rated:
raise Http404()
with transaction.atomic():
contest.rate()
return HttpResponseRedirect(request.META.get('HTTP_REFERER', reverse('admin:judge_contest_changelist')))
def get_form(self, *args, **kwargs):
form = super(ContestAdmin, self).get_form(*args, **kwargs)
perms = ('edit_own_contest', 'edit_all_contest')
form.base_fields['organizers'].queryset = Profile.objects.filter(
Q(user__is_superuser=True) |
Q(user__groups__permissions__codename__in=perms) |
Q(user__user_permissions__codename__in=perms),
).distinct()
return form
class ContestParticipationForm(ModelForm):
class Meta:
widgets = {
'contest': AdminSelect2Widget(),
'user': AdminHeavySelect2Widget(data_view='profile_select2'),
}
class ContestParticipationAdmin(admin.ModelAdmin):
fields = ('contest', 'user', 'real_start', 'virtual', 'is_disqualified')
list_display = ('contest', 'username', 'show_virtual', 'real_start', 'score', 'cumtime')
actions = ['recalculate_results']
actions_on_bottom = actions_on_top = True
search_fields = ('contest__key', 'contest__name', 'user__user__username')
form = ContestParticipationForm
date_hierarchy = 'real_start'
def get_queryset(self, request):
return super(ContestParticipationAdmin, self).get_queryset(request).only(
'contest__name', 'contest__format_name', 'contest__format_config',
'user__user__username', 'real_start', 'score', 'cumtime', 'virtual',
)
def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)
if form.changed_data and 'is_disqualified' in form.changed_data:
obj.set_disqualified(obj.is_disqualified)
def recalculate_results(self, request, queryset):
count = 0
for participation in queryset:
participation.recompute_results()
count += 1
self.message_user(request, ungettext('%d participation recalculated.',
'%d participations recalculated.',
count) % count)
recalculate_results.short_description = _('Recalculate results')
def username(self, obj):
return obj.user.username
username.short_description = _('username')
username.admin_order_field = 'user__user__username'
def show_virtual(self, obj):
return obj.virtual or '-'
show_virtual.short_description = _('virtual')
show_virtual.admin_order_field = 'virtual'

151
judge/admin/interface.py Normal file
View file

@ -0,0 +1,151 @@
from django.contrib import admin
from django.contrib.auth.models import User
from django.forms import ModelForm
from django.urls import NoReverseMatch, reverse, reverse_lazy
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from mptt.admin import DraggableMPTTAdmin
from reversion.admin import VersionAdmin
from judge.dblock import LockModel
from judge.models import NavigationBar
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
class NavigationBarAdmin(DraggableMPTTAdmin):
list_display = DraggableMPTTAdmin.list_display + ('key', 'linked_path')
fields = ('key', 'label', 'path', 'order', 'regex', 'parent')
list_editable = () # Bug in SortableModelAdmin: 500 without list_editable being set
mptt_level_indent = 20
sortable = 'order'
def __init__(self, *args, **kwargs):
super(NavigationBarAdmin, self).__init__(*args, **kwargs)
self.__save_model_calls = 0
def linked_path(self, obj):
return format_html(u'<a href="{0}" target="_blank">{0}</a>', obj.path)
linked_path.short_description = _('link path')
def save_model(self, request, obj, form, change):
self.__save_model_calls += 1
return super(NavigationBarAdmin, self).save_model(request, obj, form, change)
def changelist_view(self, request, extra_context=None):
self.__save_model_calls = 0
with NavigationBar.objects.disable_mptt_updates():
result = super(NavigationBarAdmin, self).changelist_view(request, extra_context)
if self.__save_model_calls:
with LockModel(write=(NavigationBar,)):
NavigationBar.objects.rebuild()
return result
class BlogPostForm(ModelForm):
def __init__(self, *args, **kwargs):
super(BlogPostForm, self).__init__(*args, **kwargs)
self.fields['authors'].widget.can_add_related = False
class Meta:
widgets = {
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('blog_preview'))
widgets['summary'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('blog_preview'))
class BlogPostAdmin(VersionAdmin):
fieldsets = (
(None, {'fields': ('title', 'slug', 'authors', 'visible', 'sticky', 'publish_on')}),
(_('Content'), {'fields': ('content', 'og_image')}),
(_('Summary'), {'classes': ('collapse',), 'fields': ('summary',)}),
)
prepopulated_fields = {'slug': ('title',)}
list_display = ('id', 'title', 'visible', 'sticky', 'publish_on')
list_display_links = ('id', 'title')
ordering = ('-publish_on',)
form = BlogPostForm
date_hierarchy = 'publish_on'
def has_change_permission(self, request, obj=None):
return (request.user.has_perm('judge.edit_all_post') or
request.user.has_perm('judge.change_blogpost') and (
obj is None or
obj.authors.filter(id=request.profile.id).exists()))
class SolutionForm(ModelForm):
def __init__(self, *args, **kwargs):
super(SolutionForm, self).__init__(*args, **kwargs)
self.fields['authors'].widget.can_add_related = False
class Meta:
widgets = {
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
'problem': AdminHeavySelect2Widget(data_view='problem_select2', attrs={'style': 'width: 250px'}),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('solution_preview'))
class LicenseForm(ModelForm):
class Meta:
if HeavyPreviewAdminPageDownWidget is not None:
widgets = {'text': HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('license_preview'))}
class LicenseAdmin(admin.ModelAdmin):
fields = ('key', 'link', 'name', 'display', 'icon', 'text')
list_display = ('name', 'key')
form = LicenseForm
class UserListFilter(admin.SimpleListFilter):
title = _('user')
parameter_name = 'user'
def lookups(self, request, model_admin):
return User.objects.filter(is_staff=True).values_list('id', 'username')
def queryset(self, request, queryset):
if self.value():
return queryset.filter(user_id=self.value(), user__is_staff=True)
return queryset
class LogEntryAdmin(admin.ModelAdmin):
readonly_fields = ('user', 'content_type', 'object_id', 'object_repr', 'action_flag', 'change_message')
list_display = ('__str__', 'action_time', 'user', 'content_type', 'object_link')
search_fields = ('object_repr', 'change_message')
list_filter = (UserListFilter, 'content_type')
list_display_links = None
actions = None
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return obj is None and request.user.is_superuser
def has_delete_permission(self, request, obj=None):
return False
def object_link(self, obj):
if obj.is_deletion():
link = obj.object_repr
else:
ct = obj.content_type
try:
link = format_html('<a href="{1}">{0}</a>', obj.object_repr,
reverse('admin:%s_%s_change' % (ct.app_label, ct.model), args=(obj.object_id,)))
except NoReverseMatch:
link = obj.object_repr
return link
object_link.admin_order_field = 'object_repr'
object_link.short_description = _('object')
def queryset(self, request):
return super().queryset(request).prefetch_related('content_type')

View file

@ -0,0 +1,66 @@
from django.contrib import admin
from django.forms import ModelForm
from django.urls import reverse_lazy
from django.utils.html import format_html
from django.utils.translation import gettext, gettext_lazy as _
from reversion.admin import VersionAdmin
from judge.models import Organization
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
class OrganizationForm(ModelForm):
class Meta:
widgets = {
'admins': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
'registrant': AdminHeavySelect2Widget(data_view='profile_select2'),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets['about'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('organization_preview'))
class OrganizationAdmin(VersionAdmin):
readonly_fields = ('creation_date',)
fields = ('name', 'slug', 'short_name', 'is_open', 'about', 'logo_override_image', 'slots', 'registrant',
'creation_date', 'admins')
list_display = ('name', 'short_name', 'is_open', 'slots', 'registrant', 'show_public')
prepopulated_fields = {'slug': ('name',)}
actions_on_top = True
actions_on_bottom = True
form = OrganizationForm
def show_public(self, obj):
return format_html('<a href="{0}" style="white-space:nowrap;">{1}</a>',
obj.get_absolute_url(), gettext('View on site'))
show_public.short_description = ''
def get_readonly_fields(self, request, obj=None):
fields = self.readonly_fields
if not request.user.has_perm('judge.organization_admin'):
return fields + ('registrant', 'admins', 'is_open', 'slots')
return fields
def get_queryset(self, request):
queryset = Organization.objects.all()
if request.user.has_perm('judge.edit_all_organization'):
return queryset
else:
return queryset.filter(admins=request.profile.id)
def has_change_permission(self, request, obj=None):
if not request.user.has_perm('judge.change_organization'):
return False
if request.user.has_perm('judge.edit_all_organization') or obj is None:
return True
return obj.admins.filter(id=request.profile.id).exists()
class OrganizationRequestAdmin(admin.ModelAdmin):
list_display = ('username', 'organization', 'state', 'time')
readonly_fields = ('user', 'organization')
def username(self, obj):
return obj.user.user.username
username.short_description = _('username')
username.admin_order_field = 'user__user__username'

238
judge/admin/problem.py Normal file
View file

@ -0,0 +1,238 @@
from operator import attrgetter
from django import forms
from django.contrib import admin
from django.db import transaction
from django.db.models import Q
from django.forms import ModelForm
from django.urls import reverse_lazy
from django.utils.html import format_html
from django.utils.translation import gettext, gettext_lazy as _, ungettext
from reversion.admin import VersionAdmin
from judge.models import LanguageLimit, Problem, ProblemClarification, ProblemTranslation, Profile, Solution
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminSelect2MultipleWidget, AdminSelect2Widget, \
CheckboxSelectMultipleWithSelectAll, HeavyPreviewAdminPageDownWidget, HeavyPreviewPageDownWidget
class ProblemForm(ModelForm):
change_message = forms.CharField(max_length=256, label='Edit reason', required=False)
def __init__(self, *args, **kwargs):
super(ProblemForm, self).__init__(*args, **kwargs)
self.fields['authors'].widget.can_add_related = False
self.fields['curators'].widget.can_add_related = False
self.fields['testers'].widget.can_add_related = False
self.fields['banned_users'].widget.can_add_related = False
self.fields['change_message'].widget.attrs.update({
'placeholder': gettext('Describe the changes you made (optional)'),
})
class Meta:
widgets = {
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
'curators': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
'testers': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
'banned_users': AdminHeavySelect2MultipleWidget(data_view='profile_select2',
attrs={'style': 'width: 100%'}),
'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2',
attrs={'style': 'width: 100%'}),
'types': AdminSelect2MultipleWidget,
'group': AdminSelect2Widget,
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets['description'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('problem_preview'))
class ProblemCreatorListFilter(admin.SimpleListFilter):
title = parameter_name = 'creator'
def lookups(self, request, model_admin):
queryset = Profile.objects.exclude(authored_problems=None).values_list('user__username', flat=True)
return [(name, name) for name in queryset]
def queryset(self, request, queryset):
if self.value() is None:
return queryset
return queryset.filter(authors__user__username=self.value())
class LanguageLimitInlineForm(ModelForm):
class Meta:
widgets = {'language': AdminSelect2Widget}
class LanguageLimitInline(admin.TabularInline):
model = LanguageLimit
fields = ('language', 'time_limit', 'memory_limit')
form = LanguageLimitInlineForm
class ProblemClarificationForm(ModelForm):
class Meta:
if HeavyPreviewPageDownWidget is not None:
widgets = {'description': HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'))}
class ProblemClarificationInline(admin.StackedInline):
model = ProblemClarification
fields = ('description',)
form = ProblemClarificationForm
extra = 0
class ProblemSolutionForm(ModelForm):
def __init__(self, *args, **kwargs):
super(ProblemSolutionForm, self).__init__(*args, **kwargs)
self.fields['authors'].widget.can_add_related = False
class Meta:
widgets = {
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('solution_preview'))
class ProblemSolutionInline(admin.StackedInline):
model = Solution
fields = ('is_public', 'publish_on', 'authors', 'content')
form = ProblemSolutionForm
extra = 0
class ProblemTranslationForm(ModelForm):
class Meta:
if HeavyPreviewAdminPageDownWidget is not None:
widgets = {'description': HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('problem_preview'))}
class ProblemTranslationInline(admin.StackedInline):
model = ProblemTranslation
fields = ('language', 'name', 'description')
form = ProblemTranslationForm
extra = 0
class ProblemAdmin(VersionAdmin):
fieldsets = (
(None, {
'fields': (
'code', 'name', 'is_public', 'is_manually_managed', 'date', 'authors', 'curators', 'testers',
'is_organization_private', 'organizations', 'description', 'license',
),
}),
(_('Social Media'), {'classes': ('collapse',), 'fields': ('og_image', 'summary')}),
(_('Taxonomy'), {'fields': ('types', 'group')}),
(_('Points'), {'fields': (('points', 'partial'), 'short_circuit')}),
(_('Limits'), {'fields': ('time_limit', 'memory_limit')}),
(_('Language'), {'fields': ('allowed_languages',)}),
(_('Justice'), {'fields': ('banned_users',)}),
(_('History'), {'fields': ('change_message',)}),
)
list_display = ['code', 'name', 'show_authors', 'points', 'is_public', 'show_public']
ordering = ['code']
search_fields = ('code', 'name', 'authors__user__username', 'curators__user__username')
inlines = [LanguageLimitInline, ProblemClarificationInline, ProblemSolutionInline, ProblemTranslationInline]
list_max_show_all = 1000
actions_on_top = True
actions_on_bottom = True
list_filter = ('is_public', ProblemCreatorListFilter)
form = ProblemForm
date_hierarchy = 'date'
def get_actions(self, request):
actions = super(ProblemAdmin, self).get_actions(request)
if request.user.has_perm('judge.change_public_visibility'):
func, name, desc = self.get_action('make_public')
actions[name] = (func, name, desc)
func, name, desc = self.get_action('make_private')
actions[name] = (func, name, desc)
return actions
def get_readonly_fields(self, request, obj=None):
fields = self.readonly_fields
if not request.user.has_perm('judge.change_public_visibility'):
fields += ('is_public',)
if not request.user.has_perm('judge.change_manually_managed'):
fields += ('is_manually_managed',)
return fields
def show_authors(self, obj):
return ', '.join(map(attrgetter('user.username'), obj.authors.all()))
show_authors.short_description = _('Authors')
def show_public(self, obj):
return format_html('<a href="{1}">{0}</a>', gettext('View on site'), obj.get_absolute_url())
show_public.short_description = ''
def _rescore(self, request, problem_id):
from judge.tasks import rescore_problem
transaction.on_commit(rescore_problem.s(problem_id).delay)
def make_public(self, request, queryset):
count = queryset.update(is_public=True)
for problem_id in queryset.values_list('id', flat=True):
self._rescore(request, problem_id)
self.message_user(request, ungettext('%d problem successfully marked as public.',
'%d problems successfully marked as public.',
count) % count)
make_public.short_description = _('Mark problems as public')
def make_private(self, request, queryset):
count = queryset.update(is_public=False)
for problem_id in queryset.values_list('id', flat=True):
self._rescore(request, problem_id)
self.message_user(request, ungettext('%d problem successfully marked as private.',
'%d problems successfully marked as private.',
count) % count)
make_private.short_description = _('Mark problems as private')
def get_queryset(self, request):
queryset = Problem.objects.prefetch_related('authors__user')
if request.user.has_perm('judge.edit_all_problem'):
return queryset
access = Q()
if request.user.has_perm('judge.edit_public_problem'):
access |= Q(is_public=True)
if request.user.has_perm('judge.edit_own_problem'):
access |= Q(authors__id=request.profile.id) | Q(curators__id=request.profile.id)
return queryset.filter(access).distinct() if access else queryset.none()
def has_change_permission(self, request, obj=None):
if request.user.has_perm('judge.edit_all_problem') or obj is None:
return True
if request.user.has_perm('judge.edit_public_problem') and obj.is_public:
return True
if not request.user.has_perm('judge.edit_own_problem'):
return False
return obj.is_editor(request.profile)
def formfield_for_manytomany(self, db_field, request=None, **kwargs):
if db_field.name == 'allowed_languages':
kwargs['widget'] = CheckboxSelectMultipleWithSelectAll()
return super(ProblemAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)
def get_form(self, *args, **kwargs):
form = super(ProblemAdmin, self).get_form(*args, **kwargs)
form.base_fields['authors'].queryset = Profile.objects.all()
return form
def save_model(self, request, obj, form, change):
super(ProblemAdmin, self).save_model(request, obj, form, change)
if form.changed_data and any(f in form.changed_data for f in ('is_public', 'points', 'partial')):
self._rescore(request, obj.id)
def construct_change_message(self, request, form, *args, **kwargs):
if form.cleaned_data.get('change_message'):
return form.cleaned_data['change_message']
return super(ProblemAdmin, self).construct_change_message(request, form, *args, **kwargs)

118
judge/admin/profile.py Normal file
View file

@ -0,0 +1,118 @@
from django.contrib import admin
from django.forms import ModelForm
from django.utils.html import format_html
from django.utils.translation import gettext, gettext_lazy as _, ungettext
from reversion.admin import VersionAdmin
from django_ace import AceWidget
from judge.models import Profile
from judge.widgets import AdminPagedownWidget, AdminSelect2Widget
class ProfileForm(ModelForm):
def __init__(self, *args, **kwargs):
super(ProfileForm, self).__init__(*args, **kwargs)
if 'current_contest' in self.base_fields:
# form.fields['current_contest'] does not exist when the user has only view permission on the model.
self.fields['current_contest'].queryset = self.instance.contest_history.select_related('contest') \
.only('contest__name', 'user_id', 'virtual')
self.fields['current_contest'].label_from_instance = \
lambda obj: '%s v%d' % (obj.contest.name, obj.virtual) if obj.virtual else obj.contest.name
class Meta:
widgets = {
'timezone': AdminSelect2Widget,
'language': AdminSelect2Widget,
'ace_theme': AdminSelect2Widget,
'current_contest': AdminSelect2Widget,
}
if AdminPagedownWidget is not None:
widgets['about'] = AdminPagedownWidget
class TimezoneFilter(admin.SimpleListFilter):
title = _('timezone')
parameter_name = 'timezone'
def lookups(self, request, model_admin):
return Profile.objects.values_list('timezone', 'timezone').distinct().order_by('timezone')
def queryset(self, request, queryset):
if self.value() is None:
return queryset
return queryset.filter(timezone=self.value())
class ProfileAdmin(VersionAdmin):
fields = ('user', 'display_rank', 'about', 'organizations', 'timezone', 'language', 'ace_theme',
'math_engine', 'last_access', 'ip', 'mute', 'is_unlisted', 'notes', 'is_totp_enabled', 'user_script',
'current_contest')
readonly_fields = ('user',)
list_display = ('admin_user_admin', 'email', 'is_totp_enabled', 'timezone_full',
'date_joined', 'last_access', 'ip', 'show_public')
ordering = ('user__username',)
search_fields = ('user__username', 'ip', 'user__email')
list_filter = ('language', TimezoneFilter)
actions = ('recalculate_points',)
actions_on_top = True
actions_on_bottom = True
form = ProfileForm
def get_queryset(self, request):
return super(ProfileAdmin, self).get_queryset(request).select_related('user')
def get_fields(self, request, obj=None):
if request.user.has_perm('judge.totp'):
fields = list(self.fields)
fields.insert(fields.index('is_totp_enabled') + 1, 'totp_key')
return tuple(fields)
else:
return self.fields
def get_readonly_fields(self, request, obj=None):
fields = self.readonly_fields
if not request.user.has_perm('judge.totp'):
fields += ('is_totp_enabled',)
return fields
def show_public(self, obj):
return format_html('<a href="{0}" style="white-space:nowrap;">{1}</a>',
obj.get_absolute_url(), gettext('View on site'))
show_public.short_description = ''
def admin_user_admin(self, obj):
return obj.username
admin_user_admin.admin_order_field = 'user__username'
admin_user_admin.short_description = _('User')
def email(self, obj):
return obj.user.email
email.admin_order_field = 'user__email'
email.short_description = _('Email')
def timezone_full(self, obj):
return obj.timezone
timezone_full.admin_order_field = 'timezone'
timezone_full.short_description = _('Timezone')
def date_joined(self, obj):
return obj.user.date_joined
date_joined.admin_order_field = 'user__date_joined'
date_joined.short_description = _('date joined')
def recalculate_points(self, request, queryset):
count = 0
for profile in queryset:
profile.calculate_points()
count += 1
self.message_user(request, ungettext('%d user have scores recalculated.',
'%d users have scores recalculated.',
count) % count)
recalculate_points.short_description = _('Recalculate scores')
def get_form(self, request, obj=None, **kwargs):
form = super(ProfileAdmin, self).get_form(request, obj, **kwargs)
if 'user_script' in form.base_fields:
# form.base_fields['user_script'] does not exist when the user has only view permission on the model.
form.base_fields['user_script'].widget = AceWidget('javascript', request.profile.ace_theme)
return form

120
judge/admin/runtime.py Normal file
View file

@ -0,0 +1,120 @@
from django.conf.urls import url
from django.db.models import TextField
from django.forms import ModelForm, ModelMultipleChoiceField, TextInput
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from reversion.admin import VersionAdmin
from django_ace import AceWidget
from judge.models import Judge, Problem
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminPagedownWidget
class LanguageForm(ModelForm):
problems = ModelMultipleChoiceField(
label=_('Disallowed problems'),
queryset=Problem.objects.all(),
required=False,
help_text=_('These problems are NOT allowed to be submitted in this language'),
widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2'))
class Meta:
if AdminPagedownWidget is not None:
widgets = {'description': AdminPagedownWidget}
class LanguageAdmin(VersionAdmin):
fields = ('key', 'name', 'short_name', 'common_name', 'ace', 'pygments', 'info', 'description',
'template', 'problems')
list_display = ('key', 'name', 'common_name', 'info')
form = LanguageForm
def save_model(self, request, obj, form, change):
super(LanguageAdmin, self).save_model(request, obj, form, change)
obj.problem_set.set(Problem.objects.exclude(id__in=form.cleaned_data['problems'].values('id')))
def get_form(self, request, obj=None, **kwargs):
self.form.base_fields['problems'].initial = \
Problem.objects.exclude(id__in=obj.problem_set.values('id')).values_list('pk', flat=True) if obj else []
form = super(LanguageAdmin, self).get_form(request, obj, **kwargs)
if obj is not None:
form.base_fields['template'].widget = AceWidget(obj.ace, request.profile.ace_theme)
return form
class GenerateKeyTextInput(TextInput):
def render(self, name, value, attrs=None, renderer=None):
text = super(TextInput, self).render(name, value, attrs)
return mark_safe(text + format_html(
'''\
<a href="#" onclick="return false;" class="button" id="id_{0}_regen">Regenerate</a>
<script type="text/javascript">
django.jQuery(document).ready(function ($) {{
$('#id_{0}_regen').click(function () {{
var length = 100,
charset = "abcdefghijklnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`~!@#$%^&*()_+-=|[]{{}};:,<>./?",
key = "";
for (var i = 0, n = charset.length; i < length; ++i) {{
key += charset.charAt(Math.floor(Math.random() * n));
}}
$('#id_{0}').val(key);
}});
}});
</script>
''', name))
class JudgeAdminForm(ModelForm):
class Meta:
widgets = {'auth_key': GenerateKeyTextInput}
if AdminPagedownWidget is not None:
widgets['description'] = AdminPagedownWidget
class JudgeAdmin(VersionAdmin):
form = JudgeAdminForm
readonly_fields = ('created', 'online', 'start_time', 'ping', 'load', 'last_ip', 'runtimes', 'problems')
fieldsets = (
(None, {'fields': ('name', 'auth_key', 'is_blocked')}),
(_('Description'), {'fields': ('description',)}),
(_('Information'), {'fields': ('created', 'online', 'last_ip', 'start_time', 'ping', 'load')}),
(_('Capabilities'), {'fields': ('runtimes', 'problems')}),
)
list_display = ('name', 'online', 'start_time', 'ping', 'load', 'last_ip')
ordering = ['-online', 'name']
def get_urls(self):
return ([url(r'^(\d+)/disconnect/$', self.disconnect_view, name='judge_judge_disconnect'),
url(r'^(\d+)/terminate/$', self.terminate_view, name='judge_judge_terminate')] +
super(JudgeAdmin, self).get_urls())
def disconnect_judge(self, id, force=False):
judge = get_object_or_404(Judge, id=id)
judge.disconnect(force=force)
return HttpResponseRedirect(reverse('admin:judge_judge_changelist'))
def disconnect_view(self, request, id):
return self.disconnect_judge(id)
def terminate_view(self, request, id):
return self.disconnect_judge(id, force=True)
def get_readonly_fields(self, request, obj=None):
if obj is not None and obj.online:
return self.readonly_fields + ('name',)
return self.readonly_fields
def has_delete_permission(self, request, obj=None):
result = super(JudgeAdmin, self).has_delete_permission(request, obj)
if result and obj is not None:
return not obj.online
return result
if AdminPagedownWidget is not None:
formfield_overrides = {
TextField: {'widget': AdminPagedownWidget},
}

251
judge/admin/submission.py Normal file
View file

@ -0,0 +1,251 @@
from functools import partial
from operator import itemgetter
from django.conf import settings
from django.conf.urls import url
from django.contrib import admin, messages
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.db.models import Q
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.utils.html import format_html
from django.utils.translation import gettext, gettext_lazy as _, pgettext, ungettext
from django_ace import AceWidget
from judge.models import ContestParticipation, ContestProblem, ContestSubmission, Profile, Submission, \
SubmissionSource, SubmissionTestCase
from judge.utils.raw_sql import use_straight_join
class SubmissionStatusFilter(admin.SimpleListFilter):
parameter_name = title = 'status'
__lookups = (('None', _('None')), ('NotDone', _('Not done')), ('EX', _('Exceptional'))) + Submission.STATUS
__handles = set(map(itemgetter(0), Submission.STATUS))
def lookups(self, request, model_admin):
return self.__lookups
def queryset(self, request, queryset):
if self.value() == 'None':
return queryset.filter(status=None)
elif self.value() == 'NotDone':
return queryset.exclude(status__in=['D', 'IE', 'CE', 'AB'])
elif self.value() == 'EX':
return queryset.exclude(status__in=['D', 'CE', 'G', 'AB'])
elif self.value() in self.__handles:
return queryset.filter(status=self.value())
class SubmissionResultFilter(admin.SimpleListFilter):
parameter_name = title = 'result'
__lookups = (('None', _('None')), ('BAD', _('Unaccepted'))) + Submission.RESULT
__handles = set(map(itemgetter(0), Submission.RESULT))
def lookups(self, request, model_admin):
return self.__lookups
def queryset(self, request, queryset):
if self.value() == 'None':
return queryset.filter(result=None)
elif self.value() == 'BAD':
return queryset.exclude(result='AC')
elif self.value() in self.__handles:
return queryset.filter(result=self.value())
class SubmissionTestCaseInline(admin.TabularInline):
fields = ('case', 'batch', 'status', 'time', 'memory', 'points', 'total')
readonly_fields = ('case', 'batch', 'total')
model = SubmissionTestCase
can_delete = False
max_num = 0
class ContestSubmissionInline(admin.StackedInline):
fields = ('problem', 'participation', 'points')
model = ContestSubmission
def get_formset(self, request, obj=None, **kwargs):
kwargs['formfield_callback'] = partial(self.formfield_for_dbfield, request=request, obj=obj)
return super(ContestSubmissionInline, self).get_formset(request, obj, **kwargs)
def formfield_for_dbfield(self, db_field, **kwargs):
submission = kwargs.pop('obj', None)
label = None
if submission:
if db_field.name == 'participation':
kwargs['queryset'] = ContestParticipation.objects.filter(user=submission.user,
contest__problems=submission.problem) \
.only('id', 'contest__name')
def label(obj):
return obj.contest.name
elif db_field.name == 'problem':
kwargs['queryset'] = ContestProblem.objects.filter(problem=submission.problem) \
.only('id', 'problem__name', 'contest__name')
def label(obj):
return pgettext('contest problem', '%(problem)s in %(contest)s') % {
'problem': obj.problem.name, 'contest': obj.contest.name,
}
field = super(ContestSubmissionInline, self).formfield_for_dbfield(db_field, **kwargs)
if label is not None:
field.label_from_instance = label
return field
class SubmissionSourceInline(admin.StackedInline):
fields = ('source',)
model = SubmissionSource
can_delete = False
extra = 0
def get_formset(self, request, obj=None, **kwargs):
kwargs.setdefault('widgets', {})['source'] = AceWidget(mode=obj and obj.language.ace,
theme=request.profile.ace_theme)
return super().get_formset(request, obj, **kwargs)
class SubmissionAdmin(admin.ModelAdmin):
readonly_fields = ('user', 'problem', 'date')
fields = ('user', 'problem', 'date', 'time', 'memory', 'points', 'language', 'status', 'result',
'case_points', 'case_total', 'judged_on', 'error')
actions = ('judge', 'recalculate_score')
list_display = ('id', 'problem_code', 'problem_name', 'user_column', 'execution_time', 'pretty_memory',
'points', 'language_column', 'status', 'result', 'judge_column')
list_filter = ('language', SubmissionStatusFilter, SubmissionResultFilter)
search_fields = ('problem__code', 'problem__name', 'user__user__username')
actions_on_top = True
actions_on_bottom = True
inlines = [SubmissionSourceInline, SubmissionTestCaseInline, ContestSubmissionInline]
def get_queryset(self, request):
queryset = Submission.objects.select_related('problem', 'user__user', 'language').only(
'problem__code', 'problem__name', 'user__user__username', 'language__name',
'time', 'memory', 'points', 'status', 'result',
)
use_straight_join(queryset)
if not request.user.has_perm('judge.edit_all_problem'):
id = request.profile.id
queryset = queryset.filter(Q(problem__authors__id=id) | Q(problem__curators__id=id)).distinct()
return queryset
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
if not request.user.has_perm('judge.edit_own_problem'):
return False
if request.user.has_perm('judge.edit_all_problem') or obj is None:
return True
return obj.problem.is_editor(request.profile)
def lookup_allowed(self, key, value):
return super(SubmissionAdmin, self).lookup_allowed(key, value) or key in ('problem__code',)
def judge(self, request, queryset):
if not request.user.has_perm('judge.rejudge_submission') or not request.user.has_perm('judge.edit_own_problem'):
self.message_user(request, gettext('You do not have the permission to rejudge submissions.'),
level=messages.ERROR)
return
queryset = queryset.order_by('id')
if not request.user.has_perm('judge.rejudge_submission_lot') and \
queryset.count() > settings.DMOJ_SUBMISSIONS_REJUDGE_LIMIT:
self.message_user(request, gettext('You do not have the permission to rejudge THAT many submissions.'),
level=messages.ERROR)
return
if not request.user.has_perm('judge.edit_all_problem'):
id = request.profile.id
queryset = queryset.filter(Q(problem__authors__id=id) | Q(problem__curators__id=id))
judged = len(queryset)
for model in queryset:
model.judge(rejudge=True, batch_rejudge=True)
self.message_user(request, ungettext('%d submission was successfully scheduled for rejudging.',
'%d submissions were successfully scheduled for rejudging.',
judged) % judged)
judge.short_description = _('Rejudge the selected submissions')
def recalculate_score(self, request, queryset):
if not request.user.has_perm('judge.rejudge_submission'):
self.message_user(request, gettext('You do not have the permission to rejudge submissions.'),
level=messages.ERROR)
return
submissions = list(queryset.defer(None).select_related(None).select_related('problem')
.only('points', 'case_points', 'case_total', 'problem__partial', 'problem__points'))
for submission in submissions:
submission.points = round(submission.case_points / submission.case_total * submission.problem.points
if submission.case_total else 0, 1)
if not submission.problem.partial and submission.points < submission.problem.points:
submission.points = 0
submission.save()
submission.update_contest()
for profile in Profile.objects.filter(id__in=queryset.values_list('user_id', flat=True).distinct()):
profile.calculate_points()
cache.delete('user_complete:%d' % profile.id)
cache.delete('user_attempted:%d' % profile.id)
for participation in ContestParticipation.objects.filter(
id__in=queryset.values_list('contest__participation_id')).prefetch_related('contest'):
participation.recompute_results()
self.message_user(request, ungettext('%d submission were successfully rescored.',
'%d submissions were successfully rescored.',
len(submissions)) % len(submissions))
recalculate_score.short_description = _('Rescore the selected submissions')
def problem_code(self, obj):
return obj.problem.code
problem_code.short_description = _('Problem code')
problem_code.admin_order_field = 'problem__code'
def problem_name(self, obj):
return obj.problem.name
problem_name.short_description = _('Problem name')
problem_name.admin_order_field = 'problem__name'
def user_column(self, obj):
return obj.user.user.username
user_column.admin_order_field = 'user__user__username'
user_column.short_description = _('User')
def execution_time(self, obj):
return round(obj.time, 2) if obj.time is not None else 'None'
execution_time.short_description = _('Time')
execution_time.admin_order_field = 'time'
def pretty_memory(self, obj):
memory = obj.memory
if memory is None:
return gettext('None')
if memory < 1000:
return gettext('%d KB') % memory
else:
return gettext('%.2f MB') % (memory / 1024)
pretty_memory.admin_order_field = 'memory'
pretty_memory.short_description = _('Memory')
def language_column(self, obj):
return obj.language.name
language_column.admin_order_field = 'language__name'
language_column.short_description = _('Language')
def judge_column(self, obj):
return format_html('<input type="button" value="Rejudge" onclick="location.href=\'{}/judge/\'" />', obj.id)
judge_column.short_description = ''
def get_urls(self):
return [
url(r'^(\d+)/judge/$', self.judge_view, name='judge_submission_rejudge'),
] + super(SubmissionAdmin, self).get_urls()
def judge_view(self, request, id):
if not request.user.has_perm('judge.rejudge_submission') or not request.user.has_perm('judge.edit_own_problem'):
raise PermissionDenied()
submission = get_object_or_404(Submission, id=id)
if not request.user.has_perm('judge.edit_all_problem') and \
not submission.problem.is_editor(request.profile):
raise PermissionDenied()
submission.judge(rejudge=True)
return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))

52
judge/admin/taxon.py Normal file
View file

@ -0,0 +1,52 @@
from django.contrib import admin
from django.forms import ModelForm, ModelMultipleChoiceField
from django.utils.translation import gettext_lazy as _
from judge.models import Problem
from judge.widgets import AdminHeavySelect2MultipleWidget
class ProblemGroupForm(ModelForm):
problems = ModelMultipleChoiceField(
label=_('Included problems'),
queryset=Problem.objects.all(),
required=False,
help_text=_('These problems are included in this group of problems'),
widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2'))
class ProblemGroupAdmin(admin.ModelAdmin):
fields = ('name', 'full_name', 'problems')
form = ProblemGroupForm
def save_model(self, request, obj, form, change):
super(ProblemGroupAdmin, self).save_model(request, obj, form, change)
obj.problem_set.set(form.cleaned_data['problems'])
obj.save()
def get_form(self, request, obj=None, **kwargs):
self.form.base_fields['problems'].initial = [o.pk for o in obj.problem_set.all()] if obj else []
return super(ProblemGroupAdmin, self).get_form(request, obj, **kwargs)
class ProblemTypeForm(ModelForm):
problems = ModelMultipleChoiceField(
label=_('Included problems'),
queryset=Problem.objects.all(),
required=False,
help_text=_('These problems are included in this type of problems'),
widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2'))
class ProblemTypeAdmin(admin.ModelAdmin):
fields = ('name', 'full_name', 'problems')
form = ProblemTypeForm
def save_model(self, request, obj, form, change):
super(ProblemTypeAdmin, self).save_model(request, obj, form, change)
obj.problem_set.set(form.cleaned_data['problems'])
obj.save()
def get_form(self, request, obj=None, **kwargs):
self.form.base_fields['problems'].initial = [o.pk for o in obj.problem_set.all()] if obj else []
return super(ProblemTypeAdmin, self).get_form(request, obj, **kwargs)

39
judge/admin/ticket.py Normal file
View file

@ -0,0 +1,39 @@
from django.contrib.admin import ModelAdmin
from django.contrib.admin.options import StackedInline
from django.forms import ModelForm
from django.urls import reverse_lazy
from judge.models import TicketMessage
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
class TicketMessageForm(ModelForm):
class Meta:
widgets = {
'user': AdminHeavySelect2Widget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets['body'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('ticket_preview'))
class TicketMessageInline(StackedInline):
model = TicketMessage
form = TicketMessageForm
fields = ('user', 'body')
class TicketForm(ModelForm):
class Meta:
widgets = {
'user': AdminHeavySelect2Widget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
'assignees': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
}
class TicketAdmin(ModelAdmin):
fields = ('title', 'time', 'user', 'assignees', 'content_type', 'object_id', 'notes')
readonly_fields = ('time',)
list_display = ('title', 'user', 'time', 'linked_item')
inlines = [TicketMessageInline]
form = TicketForm
date_hierarchy = 'time'

39
judge/apps.py Normal file
View file

@ -0,0 +1,39 @@
from django.apps import AppConfig
from django.db import DatabaseError
from django.utils.translation import gettext_lazy
class JudgeAppConfig(AppConfig):
name = 'judge'
verbose_name = gettext_lazy('Online Judge')
def ready(self):
# WARNING: AS THIS IS NOT A FUNCTIONAL PROGRAMMING LANGUAGE,
# OPERATIONS MAY HAVE SIDE EFFECTS.
# DO NOT REMOVE THINKING THE IMPORT IS UNUSED.
# noinspection PyUnresolvedReferences
from . import signals, jinja2 # noqa: F401, imported for side effects
from django.contrib.flatpages.models import FlatPage
from django.contrib.flatpages.admin import FlatPageAdmin
from django.contrib import admin
from reversion.admin import VersionAdmin
class FlatPageVersionAdmin(VersionAdmin, FlatPageAdmin):
pass
admin.site.unregister(FlatPage)
admin.site.register(FlatPage, FlatPageVersionAdmin)
from judge.models import Language, Profile
from django.contrib.auth.models import User
try:
lang = Language.get_python3()
for user in User.objects.filter(profile=None):
# These poor profileless users
profile = Profile(user=user, language=lang)
profile.save()
except DatabaseError:
pass

6
judge/bridge/__init__.py Normal file
View file

@ -0,0 +1,6 @@
from .djangohandler import DjangoHandler
from .djangoserver import DjangoServer
from .judgecallback import DjangoJudgeHandler
from .judgehandler import JudgeHandler
from .judgelist import JudgeList
from .judgeserver import JudgeServer

View file

@ -0,0 +1,67 @@
import json
import logging
import struct
from event_socket_server import ZlibPacketHandler
logger = logging.getLogger('judge.bridge')
size_pack = struct.Struct('!I')
class DjangoHandler(ZlibPacketHandler):
def __init__(self, server, socket):
super(DjangoHandler, self).__init__(server, socket)
self.handlers = {
'submission-request': self.on_submission,
'terminate-submission': self.on_termination,
'disconnect-judge': self.on_disconnect,
}
self._to_kill = True
# self.server.schedule(5, self._kill_if_no_request)
def _kill_if_no_request(self):
if self._to_kill:
logger.info('Killed inactive connection: %s', self._socket.getpeername())
self.close()
def _format_send(self, data):
return super(DjangoHandler, self)._format_send(json.dumps(data, separators=(',', ':')))
def packet(self, packet):
self._to_kill = False
packet = json.loads(packet)
try:
result = self.handlers.get(packet.get('name', None), self.on_malformed)(packet)
except Exception:
logger.exception('Error in packet handling (Django-facing)')
result = {'name': 'bad-request'}
self.send(result, self._schedule_close)
def _schedule_close(self):
self.server.schedule(0, self.close)
def on_submission(self, data):
id = data['submission-id']
problem = data['problem-id']
language = data['language']
source = data['source']
priority = data['priority']
if not self.server.judges.check_priority(priority):
return {'name': 'bad-request'}
self.server.judges.judge(id, problem, language, source, priority)
return {'name': 'submission-received', 'submission-id': id}
def on_termination(self, data):
return {'name': 'submission-received', 'judge-aborted': self.server.judges.abort(data['submission-id'])}
def on_disconnect(self, data):
judge_id = data['judge-id']
force = data['force']
self.server.judges.disconnect(judge_id, force=force)
def on_malformed(self, packet):
logger.error('Malformed packet: %s', packet)
def on_close(self):
self._to_kill = False

View file

@ -0,0 +1,7 @@
from event_socket_server import get_preferred_engine
class DjangoServer(get_preferred_engine()):
def __init__(self, judges, *args, **kwargs):
super(DjangoServer, self).__init__(*args, **kwargs)
self.judges = judges

View file

@ -0,0 +1,411 @@
import json
import logging
import time
from operator import itemgetter
from django import db
from django.utils import timezone
from judge import event_poster as event
from judge.caching import finished_submission
from judge.models import Judge, Language, LanguageLimit, Problem, RuntimeVersion, Submission, SubmissionTestCase
from .judgehandler import JudgeHandler, SubmissionData
logger = logging.getLogger('judge.bridge')
json_log = logging.getLogger('judge.json.bridge')
UPDATE_RATE_LIMIT = 5
UPDATE_RATE_TIME = 0.5
def _ensure_connection():
try:
db.connection.cursor().execute('SELECT 1').fetchall()
except Exception:
db.connection.close()
class DjangoJudgeHandler(JudgeHandler):
def __init__(self, server, socket):
super(DjangoJudgeHandler, self).__init__(server, socket)
# each value is (updates, last reset)
self.update_counter = {}
self.judge = None
self.judge_address = None
self._submission_cache_id = None
self._submission_cache = {}
json_log.info(self._make_json_log(action='connect'))
def on_close(self):
super(DjangoJudgeHandler, self).on_close()
json_log.info(self._make_json_log(action='disconnect', info='judge disconnected'))
if self._working:
Submission.objects.filter(id=self._working).update(status='IE', result='IE')
json_log.error(self._make_json_log(sub=self._working, action='close', info='IE due to shutdown on grading'))
def on_malformed(self, packet):
super(DjangoJudgeHandler, self).on_malformed(packet)
json_log.exception(self._make_json_log(sub=self._working, info='malformed zlib packet'))
def _packet_exception(self):
json_log.exception(self._make_json_log(sub=self._working, info='packet processing exception'))
def get_related_submission_data(self, submission):
_ensure_connection() # We are called from the django-facing daemon thread. Guess what happens.
try:
pid, time, memory, short_circuit, lid, is_pretested, sub_date, uid, part_virtual, part_id = (
Submission.objects.filter(id=submission)
.values_list('problem__id', 'problem__time_limit', 'problem__memory_limit',
'problem__short_circuit', 'language__id', 'is_pretested', 'date', 'user__id',
'contest__participation__virtual', 'contest__participation__id')).get()
except Submission.DoesNotExist:
logger.error('Submission vanished: %s', submission)
json_log.error(self._make_json_log(
sub=self._working, action='request',
info='submission vanished when fetching info',
))
return
attempt_no = Submission.objects.filter(problem__id=pid, contest__participation__id=part_id, user__id=uid,
date__lt=sub_date).exclude(status__in=('CE', 'IE')).count() + 1
try:
time, memory = (LanguageLimit.objects.filter(problem__id=pid, language__id=lid)
.values_list('time_limit', 'memory_limit').get())
except LanguageLimit.DoesNotExist:
pass
return SubmissionData(
time=time,
memory=memory,
short_circuit=short_circuit,
pretests_only=is_pretested,
contest_no=part_virtual,
attempt_no=attempt_no,
user_id=uid,
)
def _authenticate(self, id, key):
result = Judge.objects.filter(name=id, auth_key=key, is_blocked=False).exists()
if not result:
json_log.warning(self._make_json_log(action='auth', judge=id, info='judge failed authentication'))
return result
def _connected(self):
judge = self.judge = Judge.objects.get(name=self.name)
judge.start_time = timezone.now()
judge.online = True
judge.problems.set(Problem.objects.filter(code__in=list(self.problems.keys())))
judge.runtimes.set(Language.objects.filter(key__in=list(self.executors.keys())))
# Delete now in case we somehow crashed and left some over from the last connection
RuntimeVersion.objects.filter(judge=judge).delete()
versions = []
for lang in judge.runtimes.all():
versions += [
RuntimeVersion(language=lang, name=name, version='.'.join(map(str, version)), priority=idx, judge=judge)
for idx, (name, version) in enumerate(self.executors[lang.key])
]
RuntimeVersion.objects.bulk_create(versions)
judge.last_ip = self.client_address[0]
judge.save()
self.judge_address = '[%s]:%s' % (self.client_address[0], self.client_address[1])
json_log.info(self._make_json_log(action='auth', info='judge successfully authenticated',
executors=list(self.executors.keys())))
def _disconnected(self):
Judge.objects.filter(id=self.judge.id).update(online=False)
RuntimeVersion.objects.filter(judge=self.judge).delete()
def _update_ping(self):
try:
Judge.objects.filter(name=self.name).update(ping=self.latency, load=self.load)
except Exception as e:
# What can I do? I don't want to tie this to MySQL.
if e.__class__.__name__ == 'OperationalError' and e.__module__ == '_mysql_exceptions' and e.args[0] == 2006:
db.connection.close()
def _post_update_submission(self, id, state, done=False):
if self._submission_cache_id == id:
data = self._submission_cache
else:
self._submission_cache = data = Submission.objects.filter(id=id).values(
'problem__is_public', 'contest__participation__contest__key',
'user_id', 'problem_id', 'status', 'language__key',
).get()
self._submission_cache_id = id
if data['problem__is_public']:
event.post('submissions', {
'type': 'done-submission' if done else 'update-submission',
'state': state, 'id': id,
'contest': data['contest__participation__contest__key'],
'user': data['user_id'], 'problem': data['problem_id'],
'status': data['status'], 'language': data['language__key'],
})
def on_submission_processing(self, packet):
id = packet['submission-id']
if Submission.objects.filter(id=id).update(status='P', judged_on=self.judge):
event.post('sub_%s' % Submission.get_id_secret(id), {'type': 'processing'})
self._post_update_submission(id, 'processing')
json_log.info(self._make_json_log(packet, action='processing'))
else:
logger.warning('Unknown submission: %s', id)
json_log.error(self._make_json_log(packet, action='processing', info='unknown submission'))
def on_submission_wrong_acknowledge(self, packet, expected, got):
json_log.error(self._make_json_log(packet, action='processing', info='wrong-acknowledge', expected=expected))
def on_grading_begin(self, packet):
super(DjangoJudgeHandler, self).on_grading_begin(packet)
if Submission.objects.filter(id=packet['submission-id']).update(
status='G', is_pretested=packet['pretested'],
current_testcase=1, batch=False):
SubmissionTestCase.objects.filter(submission_id=packet['submission-id']).delete()
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'grading-begin'})
self._post_update_submission(packet['submission-id'], 'grading-begin')
json_log.info(self._make_json_log(packet, action='grading-begin'))
else:
logger.warning('Unknown submission: %s', packet['submission-id'])
json_log.error(self._make_json_log(packet, action='grading-begin', info='unknown submission'))
def _submission_is_batch(self, id):
if not Submission.objects.filter(id=id).update(batch=True):
logger.warning('Unknown submission: %s', id)
def on_grading_end(self, packet):
super(DjangoJudgeHandler, self).on_grading_end(packet)
try:
submission = Submission.objects.get(id=packet['submission-id'])
except Submission.DoesNotExist:
logger.warning('Unknown submission: %s', packet['submission-id'])
json_log.error(self._make_json_log(packet, action='grading-end', info='unknown submission'))
return
time = 0
memory = 0
points = 0.0
total = 0
status = 0
status_codes = ['SC', 'AC', 'WA', 'MLE', 'TLE', 'IR', 'RTE', 'OLE']
batches = {} # batch number: (points, total)
for case in SubmissionTestCase.objects.filter(submission=submission):
time += case.time
if not case.batch:
points += case.points
total += case.total
else:
if case.batch in batches:
batches[case.batch][0] = min(batches[case.batch][0], case.points)
batches[case.batch][1] = max(batches[case.batch][1], case.total)
else:
batches[case.batch] = [case.points, case.total]
memory = max(memory, case.memory)
i = status_codes.index(case.status)
if i > status:
status = i
for i in batches:
points += batches[i][0]
total += batches[i][1]
points = round(points, 1)
total = round(total, 1)
submission.case_points = points
submission.case_total = total
problem = submission.problem
sub_points = round(points / total * problem.points if total > 0 else 0, 3)
if not problem.partial and sub_points != problem.points:
sub_points = 0
submission.status = 'D'
submission.time = time
submission.memory = memory
submission.points = sub_points
submission.result = status_codes[status]
submission.save()
json_log.info(self._make_json_log(
packet, action='grading-end', time=time, memory=memory,
points=sub_points, total=problem.points, result=submission.result,
case_points=points, case_total=total, user=submission.user_id,
problem=problem.code, finish=True,
))
submission.user._updating_stats_only = True
submission.user.calculate_points()
problem._updating_stats_only = True
problem.update_stats()
submission.update_contest()
finished_submission(submission)
event.post('sub_%s' % submission.id_secret, {
'type': 'grading-end',
'time': time,
'memory': memory,
'points': float(points),
'total': float(problem.points),
'result': submission.result,
})
if hasattr(submission, 'contest'):
participation = submission.contest.participation
event.post('contest_%d' % participation.contest_id, {'type': 'update'})
self._post_update_submission(submission.id, 'grading-end', done=True)
def on_compile_error(self, packet):
super(DjangoJudgeHandler, self).on_compile_error(packet)
if Submission.objects.filter(id=packet['submission-id']).update(status='CE', result='CE', error=packet['log']):
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {
'type': 'compile-error',
'log': packet['log'],
})
self._post_update_submission(packet['submission-id'], 'compile-error', done=True)
json_log.info(self._make_json_log(packet, action='compile-error', log=packet['log'],
finish=True, result='CE'))
else:
logger.warning('Unknown submission: %s', packet['submission-id'])
json_log.error(self._make_json_log(packet, action='compile-error', info='unknown submission',
log=packet['log'], finish=True, result='CE'))
def on_compile_message(self, packet):
super(DjangoJudgeHandler, self).on_compile_message(packet)
if Submission.objects.filter(id=packet['submission-id']).update(error=packet['log']):
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'compile-message'})
json_log.info(self._make_json_log(packet, action='compile-message', log=packet['log']))
else:
logger.warning('Unknown submission: %s', packet['submission-id'])
json_log.error(self._make_json_log(packet, action='compile-message', info='unknown submission',
log=packet['log']))
def on_internal_error(self, packet):
super(DjangoJudgeHandler, self).on_internal_error(packet)
id = packet['submission-id']
if Submission.objects.filter(id=id).update(status='IE', result='IE', error=packet['message']):
event.post('sub_%s' % Submission.get_id_secret(id), {'type': 'internal-error'})
self._post_update_submission(id, 'internal-error', done=True)
json_log.info(self._make_json_log(packet, action='internal-error', message=packet['message'],
finish=True, result='IE'))
else:
logger.warning('Unknown submission: %s', id)
json_log.error(self._make_json_log(packet, action='internal-error', info='unknown submission',
message=packet['message'], finish=True, result='IE'))
def on_submission_terminated(self, packet):
super(DjangoJudgeHandler, self).on_submission_terminated(packet)
if Submission.objects.filter(id=packet['submission-id']).update(status='AB', result='AB'):
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'aborted-submission'})
self._post_update_submission(packet['submission-id'], 'terminated', done=True)
json_log.info(self._make_json_log(packet, action='aborted', finish=True, result='AB'))
else:
logger.warning('Unknown submission: %s', packet['submission-id'])
json_log.error(self._make_json_log(packet, action='aborted', info='unknown submission',
finish=True, result='AB'))
def on_batch_begin(self, packet):
super(DjangoJudgeHandler, self).on_batch_begin(packet)
json_log.info(self._make_json_log(packet, action='batch-begin', batch=self.batch_id))
def on_batch_end(self, packet):
super(DjangoJudgeHandler, self).on_batch_end(packet)
json_log.info(self._make_json_log(packet, action='batch-end', batch=self.batch_id))
def on_test_case(self, packet, max_feedback=SubmissionTestCase._meta.get_field('feedback').max_length):
super(DjangoJudgeHandler, self).on_test_case(packet)
id = packet['submission-id']
updates = packet['cases']
max_position = max(map(itemgetter('position'), updates))
if not Submission.objects.filter(id=id).update(current_testcase=max_position + 1):
logger.warning('Unknown submission: %s', id)
json_log.error(self._make_json_log(packet, action='test-case', info='unknown submission'))
return
bulk_test_case_updates = []
for result in updates:
test_case = SubmissionTestCase(submission_id=id, case=result['position'])
status = result['status']
if status & 4:
test_case.status = 'TLE'
elif status & 8:
test_case.status = 'MLE'
elif status & 64:
test_case.status = 'OLE'
elif status & 2:
test_case.status = 'RTE'
elif status & 16:
test_case.status = 'IR'
elif status & 1:
test_case.status = 'WA'
elif status & 32:
test_case.status = 'SC'
else:
test_case.status = 'AC'
test_case.time = result['time']
test_case.memory = result['memory']
test_case.points = result['points']
test_case.total = result['total-points']
test_case.batch = self.batch_id if self.in_batch else None
test_case.feedback = (result.get('feedback') or '')[:max_feedback]
test_case.extended_feedback = result.get('extended-feedback') or ''
test_case.output = result['output']
bulk_test_case_updates.append(test_case)
json_log.info(self._make_json_log(
packet, action='test-case', case=test_case.case, batch=test_case.batch,
time=test_case.time, memory=test_case.memory, feedback=test_case.feedback,
extended_feedback=test_case.extended_feedback, output=test_case.output,
points=test_case.points, total=test_case.total, status=test_case.status,
))
do_post = True
if id in self.update_counter:
cnt, reset = self.update_counter[id]
cnt += 1
if time.monotonic() - reset > UPDATE_RATE_TIME:
del self.update_counter[id]
else:
self.update_counter[id] = (cnt, reset)
if cnt > UPDATE_RATE_LIMIT:
do_post = False
if id not in self.update_counter:
self.update_counter[id] = (1, time.monotonic())
if do_post:
event.post('sub_%s' % Submission.get_id_secret(id), {
'type': 'test-case',
'id': max_position,
})
self._post_update_submission(id, state='test-case')
SubmissionTestCase.objects.bulk_create(bulk_test_case_updates)
def on_supported_problems(self, packet):
super(DjangoJudgeHandler, self).on_supported_problems(packet)
self.judge.problems.set(Problem.objects.filter(code__in=list(self.problems.keys())))
json_log.info(self._make_json_log(action='update-problems', count=len(self.problems)))
def _make_json_log(self, packet=None, sub=None, **kwargs):
data = {
'judge': self.name,
'address': self.judge_address,
}
if sub is None and packet is not None:
sub = packet.get('submission-id')
if sub is not None:
data['submission'] = sub
data.update(kwargs)
return json.dumps(data)

View file

@ -0,0 +1,268 @@
import json
import logging
import time
from collections import deque, namedtuple
from event_socket_server import ProxyProtocolMixin, ZlibPacketHandler
logger = logging.getLogger('judge.bridge')
SubmissionData = namedtuple('SubmissionData', 'time memory short_circuit pretests_only contest_no attempt_no user_id')
class JudgeHandler(ProxyProtocolMixin, ZlibPacketHandler):
def __init__(self, server, socket):
super(JudgeHandler, self).__init__(server, socket)
self.handlers = {
'grading-begin': self.on_grading_begin,
'grading-end': self.on_grading_end,
'compile-error': self.on_compile_error,
'compile-message': self.on_compile_message,
'batch-begin': self.on_batch_begin,
'batch-end': self.on_batch_end,
'test-case-status': self.on_test_case,
'internal-error': self.on_internal_error,
'submission-terminated': self.on_submission_terminated,
'submission-acknowledged': self.on_submission_acknowledged,
'ping-response': self.on_ping_response,
'supported-problems': self.on_supported_problems,
'handshake': self.on_handshake,
}
self._to_kill = True
self._working = False
self._no_response_job = None
self._problems = []
self.executors = []
self.problems = {}
self.latency = None
self.time_delta = None
self.load = 1e100
self.name = None
self.batch_id = None
self.in_batch = False
self._ping_average = deque(maxlen=6) # 1 minute average, just like load
self._time_delta = deque(maxlen=6)
self.server.schedule(15, self._kill_if_no_auth)
logger.info('Judge connected from: %s', self.client_address)
def _kill_if_no_auth(self):
if self._to_kill:
logger.info('Judge not authenticated: %s', self.client_address)
self.close()
def on_close(self):
self._to_kill = False
if self._no_response_job:
self.server.unschedule(self._no_response_job)
self.server.judges.remove(self)
if self.name is not None:
self._disconnected()
logger.info('Judge disconnected from: %s', self.client_address)
def _authenticate(self, id, key):
return False
def _connected(self):
pass
def _disconnected(self):
pass
def _update_ping(self):
pass
def _format_send(self, data):
return super(JudgeHandler, self)._format_send(json.dumps(data, separators=(',', ':')))
def on_handshake(self, packet):
if 'id' not in packet or 'key' not in packet:
logger.warning('Malformed handshake: %s', self.client_address)
self.close()
return
if not self._authenticate(packet['id'], packet['key']):
logger.warning('Authentication failure: %s', self.client_address)
self.close()
return
self._to_kill = False
self._problems = packet['problems']
self.problems = dict(self._problems)
self.executors = packet['executors']
self.name = packet['id']
self.send({'name': 'handshake-success'})
logger.info('Judge authenticated: %s (%s)', self.client_address, packet['id'])
self.server.judges.register(self)
self._connected()
def can_judge(self, problem, executor):
return problem in self.problems and executor in self.executors
@property
def working(self):
return bool(self._working)
def get_related_submission_data(self, submission):
return SubmissionData(
time=2,
memory=16384,
short_circuit=False,
pretests_only=False,
contest_no=None,
attempt_no=1,
user_id=None,
)
def disconnect(self, force=False):
if force:
# Yank the power out.
self.close()
else:
self.send({'name': 'disconnect'})
def submit(self, id, problem, language, source):
data = self.get_related_submission_data(id)
self._working = id
self._no_response_job = self.server.schedule(20, self._kill_if_no_response)
self.send({
'name': 'submission-request',
'submission-id': id,
'problem-id': problem,
'language': language,
'source': source,
'time-limit': data.time,
'memory-limit': data.memory,
'short-circuit': data.short_circuit,
'meta': {
'pretests-only': data.pretests_only,
'in-contest': data.contest_no,
'attempt-no': data.attempt_no,
'user': data.user_id,
},
})
def _kill_if_no_response(self):
logger.error('Judge seems dead: %s: %s', self.name, self._working)
self.close()
def malformed_packet(self, exception):
logger.exception('Judge sent malformed packet: %s', self.name)
super(JudgeHandler, self).malformed_packet(exception)
def on_submission_processing(self, packet):
pass
def on_submission_wrong_acknowledge(self, packet, expected, got):
pass
def on_submission_acknowledged(self, packet):
if not packet.get('submission-id', None) == self._working:
logger.error('Wrong acknowledgement: %s: %s, expected: %s', self.name, packet.get('submission-id', None),
self._working)
self.on_submission_wrong_acknowledge(packet, self._working, packet.get('submission-id', None))
self.close()
logger.info('Submission acknowledged: %d', self._working)
if self._no_response_job:
self.server.unschedule(self._no_response_job)
self._no_response_job = None
self.on_submission_processing(packet)
def abort(self):
self.send({'name': 'terminate-submission'})
def get_current_submission(self):
return self._working or None
def ping(self):
self.send({'name': 'ping', 'when': time.time()})
def packet(self, data):
try:
try:
data = json.loads(data)
if 'name' not in data:
raise ValueError
except ValueError:
self.on_malformed(data)
else:
handler = self.handlers.get(data['name'], self.on_malformed)
handler(data)
except Exception:
logger.exception('Error in packet handling (Judge-side): %s', self.name)
self._packet_exception()
# You can't crash here because you aren't so sure about the judges
# not being malicious or simply malforms. THIS IS A SERVER!
def _packet_exception(self):
pass
def _submission_is_batch(self, id):
pass
def on_supported_problems(self, packet):
logger.info('%s: Updated problem list', self.name)
self._problems = packet['problems']
self.problems = dict(self._problems)
if not self.working:
self.server.judges.update_problems(self)
def on_grading_begin(self, packet):
logger.info('%s: Grading has begun on: %s', self.name, packet['submission-id'])
self.batch_id = None
def on_grading_end(self, packet):
logger.info('%s: Grading has ended on: %s', self.name, packet['submission-id'])
self._free_self(packet)
self.batch_id = None
def on_compile_error(self, packet):
logger.info('%s: Submission failed to compile: %s', self.name, packet['submission-id'])
self._free_self(packet)
def on_compile_message(self, packet):
logger.info('%s: Submission generated compiler messages: %s', self.name, packet['submission-id'])
def on_internal_error(self, packet):
try:
raise ValueError('\n\n' + packet['message'])
except ValueError:
logger.exception('Judge %s failed while handling submission %s', self.name, packet['submission-id'])
self._free_self(packet)
def on_submission_terminated(self, packet):
logger.info('%s: Submission aborted: %s', self.name, packet['submission-id'])
self._free_self(packet)
def on_batch_begin(self, packet):
logger.info('%s: Batch began on: %s', self.name, packet['submission-id'])
self.in_batch = True
if self.batch_id is None:
self.batch_id = 0
self._submission_is_batch(packet['submission-id'])
self.batch_id += 1
def on_batch_end(self, packet):
self.in_batch = False
logger.info('%s: Batch ended on: %s', self.name, packet['submission-id'])
def on_test_case(self, packet):
logger.info('%s: %d test case(s) completed on: %s', self.name, len(packet['cases']), packet['submission-id'])
def on_malformed(self, packet):
logger.error('%s: Malformed packet: %s', self.name, packet)
def on_ping_response(self, packet):
end = time.time()
self._ping_average.append(end - packet['when'])
self._time_delta.append((end + packet['when']) / 2 - packet['time'])
self.latency = sum(self._ping_average) / len(self._ping_average)
self.time_delta = sum(self._time_delta) / len(self._time_delta)
self.load = packet['load']
self._update_ping()
def _free_self(self, packet):
self._working = False
self.server.judges.on_judge_free(self, packet['submission-id'])

123
judge/bridge/judgelist.py Normal file
View file

@ -0,0 +1,123 @@
import logging
from collections import namedtuple
from operator import attrgetter
from threading import RLock
try:
from llist import dllist
except ImportError:
from pyllist import dllist
logger = logging.getLogger('judge.bridge')
PriorityMarker = namedtuple('PriorityMarker', 'priority')
class JudgeList(object):
priorities = 4
def __init__(self):
self.queue = dllist()
self.priority = [self.queue.append(PriorityMarker(i)) for i in range(self.priorities)]
self.judges = set()
self.node_map = {}
self.submission_map = {}
self.lock = RLock()
def _handle_free_judge(self, judge):
with self.lock:
node = self.queue.first
while node:
if not isinstance(node.value, PriorityMarker):
id, problem, language, source = node.value
if judge.can_judge(problem, language):
self.submission_map[id] = judge
logger.info('Dispatched queued submission %d: %s', id, judge.name)
try:
judge.submit(id, problem, language, source)
except Exception:
logger.exception('Failed to dispatch %d (%s, %s) to %s', id, problem, language, judge.name)
self.judges.remove(judge)
return
self.queue.remove(node)
del self.node_map[id]
break
node = node.next
def register(self, judge):
with self.lock:
# Disconnect all judges with the same name, see <https://github.com/DMOJ/online-judge/issues/828>
self.disconnect(judge, force=True)
self.judges.add(judge)
self._handle_free_judge(judge)
def disconnect(self, judge_id, force=False):
for judge in self.judges:
if judge.name == judge_id:
judge.disconnect(force=force)
def update_problems(self, judge):
with self.lock:
self._handle_free_judge(judge)
def remove(self, judge):
with self.lock:
sub = judge.get_current_submission()
if sub is not None:
try:
del self.submission_map[sub]
except KeyError:
pass
self.judges.discard(judge)
def __iter__(self):
return iter(self.judges)
def on_judge_free(self, judge, submission):
with self.lock:
logger.info('Judge available after grading %d: %s', submission, judge.name)
del self.submission_map[submission]
self._handle_free_judge(judge)
def abort(self, submission):
with self.lock:
logger.info('Abort request: %d', submission)
try:
self.submission_map[submission].abort()
return True
except KeyError:
try:
node = self.node_map[submission]
except KeyError:
pass
else:
self.queue.remove(node)
del self.node_map[submission]
return False
def check_priority(self, priority):
return 0 <= priority < self.priorities
def judge(self, id, problem, language, source, priority):
with self.lock:
if id in self.submission_map or id in self.node_map:
# Already judging, don't queue again. This can happen during batch rejudges, rejudges should be
# idempotent.
return
candidates = [judge for judge in self.judges if not judge.working and judge.can_judge(problem, language)]
logger.info('Free judges: %d', len(candidates))
if candidates:
# Schedule the submission on the judge reporting least load.
judge = min(candidates, key=attrgetter('load'))
logger.info('Dispatched submission %d to: %s', id, judge.name)
self.submission_map[id] = judge
try:
judge.submit(id, problem, language, source)
except Exception:
logger.exception('Failed to dispatch %d (%s, %s) to %s', id, problem, language, judge.name)
self.judges.discard(judge)
return self.judge(id, problem, language, source, priority)
else:
self.node_map[id] = self.queue.insert((id, problem, language, source), self.priority[priority])
logger.info('Queued submission: %d', id)

View file

@ -0,0 +1,68 @@
import logging
import os
import threading
import time
from event_socket_server import get_preferred_engine
from judge.models import Judge
from .judgelist import JudgeList
logger = logging.getLogger('judge.bridge')
def reset_judges():
Judge.objects.update(online=False, ping=None, load=None)
class JudgeServer(get_preferred_engine()):
def __init__(self, *args, **kwargs):
super(JudgeServer, self).__init__(*args, **kwargs)
reset_judges()
self.judges = JudgeList()
self.ping_judge_thread = threading.Thread(target=self.ping_judge, args=())
self.ping_judge_thread.daemon = True
self.ping_judge_thread.start()
def on_shutdown(self):
super(JudgeServer, self).on_shutdown()
reset_judges()
def ping_judge(self):
try:
while True:
for judge in self.judges:
judge.ping()
time.sleep(10)
except Exception:
logger.exception('Ping error')
raise
def main():
import argparse
import logging
from .judgehandler import JudgeHandler
format = '%(asctime)s:%(levelname)s:%(name)s:%(message)s'
logging.basicConfig(format=format)
logging.getLogger().setLevel(logging.INFO)
handler = logging.FileHandler(os.path.join(os.path.dirname(__file__), 'judgeserver.log'), encoding='utf-8')
handler.setFormatter(logging.Formatter(format))
handler.setLevel(logging.INFO)
logging.getLogger().addHandler(handler)
parser = argparse.ArgumentParser(description='''
Runs the bridge between DMOJ website and judges.
''')
parser.add_argument('judge_host', nargs='+', action='append',
help='host to listen for the judge')
parser.add_argument('-p', '--judge-port', type=int, action='append',
help='port to listen for the judge')
args = parser.parse_args()
server = JudgeServer(list(zip(args.judge_host, args.judge_port)), JudgeHandler)
server.serve_forever()
if __name__ == '__main__':
main()

10
judge/caching.py Normal file
View file

@ -0,0 +1,10 @@
from django.core.cache import cache
def finished_submission(sub):
keys = ['user_complete:%d' % sub.user_id, 'user_attempted:%s' % sub.user_id]
if hasattr(sub, 'contest'):
participation = sub.contest.participation
keys += ['contest_complete:%d' % participation.id]
keys += ['contest_attempted:%d' % participation.id]
cache.delete_many(keys)

122
judge/comments.py Normal file
View file

@ -0,0 +1,122 @@
from django import forms
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db.models import Count
from django.db.models.expressions import F, Value
from django.db.models.functions import Coalesce
from django.forms import ModelForm
from django.http import HttpResponseForbidden, HttpResponseNotFound, HttpResponseRedirect
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _
from django.views.generic import View
from django.views.generic.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin
from reversion import revisions
from reversion.models import Revision, Version
from judge.dblock import LockModel
from judge.models import Comment, CommentLock, CommentVote
from judge.utils.raw_sql import RawSQLColumn, unique_together_left_join
from judge.widgets import HeavyPreviewPageDownWidget
class CommentForm(ModelForm):
class Meta:
model = Comment
fields = ['body', 'parent']
widgets = {
'parent': forms.HiddenInput(),
}
if HeavyPreviewPageDownWidget is not None:
widgets['body'] = HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'),
preview_timeout=1000, hide_preview_button=True)
def __init__(self, request, *args, **kwargs):
self.request = request
super(CommentForm, self).__init__(*args, **kwargs)
self.fields['body'].widget.attrs.update({'placeholder': _('Comment body')})
def clean(self):
if self.request is not None and self.request.user.is_authenticated:
profile = self.request.profile
if profile.mute:
raise ValidationError(_('Your part is silent, little toad.'))
elif (not self.request.user.is_staff and
not profile.submission_set.filter(points=F('problem__points')).exists()):
raise ValidationError(_('You need to have solved at least one problem '
'before your voice can be heard.'))
return super(CommentForm, self).clean()
class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
comment_page = None
def get_comment_page(self):
if self.comment_page is None:
raise NotImplementedError()
return self.comment_page
def is_comment_locked(self):
return (CommentLock.objects.filter(page=self.get_comment_page()).exists() and
not self.request.user.has_perm('judge.override_comment_lock'))
@method_decorator(login_required)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
page = self.get_comment_page()
if self.is_comment_locked():
return HttpResponseForbidden()
parent = request.POST.get('parent')
if parent:
try:
parent = int(parent)
except ValueError:
return HttpResponseNotFound()
else:
if not Comment.objects.filter(hidden=False, id=parent, page=page).exists():
return HttpResponseNotFound()
form = CommentForm(request, request.POST)
if form.is_valid():
comment = form.save(commit=False)
comment.author = request.profile
comment.page = page
with LockModel(write=(Comment, Revision, Version), read=(ContentType,)), revisions.create_revision():
revisions.set_user(request.user)
revisions.set_comment(_('Posted comment'))
comment.save()
return HttpResponseRedirect(request.path)
context = self.get_context_data(object=self.object, comment_form=form)
return self.render_to_response(context)
def get(self, request, *args, **kwargs):
self.object = self.get_object()
return self.render_to_response(self.get_context_data(
object=self.object,
comment_form=CommentForm(request, initial={'page': self.get_comment_page(), 'parent': None}),
))
def get_context_data(self, **kwargs):
context = super(CommentedDetailView, self).get_context_data(**kwargs)
queryset = Comment.objects.filter(hidden=False, page=self.get_comment_page())
context['has_comments'] = queryset.exists()
context['comment_lock'] = self.is_comment_locked()
queryset = queryset.select_related('author__user').defer('author__about').annotate(revisions=Count('versions'))
if self.request.user.is_authenticated:
queryset = queryset.annotate(vote_score=Coalesce(RawSQLColumn(CommentVote, 'score'), Value(0)))
profile = self.request.profile
unique_together_left_join(queryset, CommentVote, 'comment', 'voter', profile.id)
context['is_new_user'] = (not self.request.user.is_staff and
not profile.submission_set.filter(points=F('problem__points')).exists())
context['comment_list'] = queryset
context['vote_hide_threshold'] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD
return context

View file

@ -0,0 +1,5 @@
from judge.contest_format.atcoder import AtCoderContestFormat
from judge.contest_format.default import DefaultContestFormat
from judge.contest_format.ecoo import ECOOContestFormat
from judge.contest_format.ioi import IOIContestFormat
from judge.contest_format.registry import choices, formats

View file

@ -0,0 +1,113 @@
from datetime import timedelta
from django.core.exceptions import ValidationError
from django.db import connection
from django.template.defaultfilters import floatformat
from django.urls import reverse
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy
from judge.contest_format.default import DefaultContestFormat
from judge.contest_format.registry import register_contest_format
from judge.timezone import from_database_time
from judge.utils.timedelta import nice_repr
@register_contest_format('atcoder')
class AtCoderContestFormat(DefaultContestFormat):
name = gettext_lazy('AtCoder')
config_defaults = {'penalty': 5}
config_validators = {'penalty': lambda x: x >= 0}
'''
penalty: Number of penalty minutes each incorrect submission adds. Defaults to 5.
'''
@classmethod
def validate(cls, config):
if config is None:
return
if not isinstance(config, dict):
raise ValidationError('AtCoder-styled contest expects no config or dict as config')
for key, value in config.items():
if key not in cls.config_defaults:
raise ValidationError('unknown config key "%s"' % key)
if not isinstance(value, type(cls.config_defaults[key])):
raise ValidationError('invalid type for config key "%s"' % key)
if not cls.config_validators[key](value):
raise ValidationError('invalid value "%s" for config key "%s"' % (value, key))
def __init__(self, contest, config):
self.config = self.config_defaults.copy()
self.config.update(config or {})
self.contest = contest
def update_participation(self, participation):
cumtime = 0
penalty = 0
points = 0
format_data = {}
with connection.cursor() as cursor:
cursor.execute('''
SELECT MAX(cs.points) as `score`, (
SELECT MIN(csub.date)
FROM judge_contestsubmission ccs LEFT OUTER JOIN
judge_submission csub ON (csub.id = ccs.submission_id)
WHERE ccs.problem_id = cp.id AND ccs.participation_id = %s AND ccs.points = MAX(cs.points)
) AS `time`, cp.id AS `prob`
FROM judge_contestproblem cp INNER JOIN
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
judge_submission sub ON (sub.id = cs.submission_id)
GROUP BY cp.id
''', (participation.id, participation.id))
for score, time, prob in cursor.fetchall():
time = from_database_time(time)
dt = (time - participation.start).total_seconds()
# Compute penalty
if self.config['penalty']:
# An IE can have a submission result of `None`
subs = participation.submissions.exclude(submission__result__isnull=True) \
.exclude(submission__result__in=['IE', 'CE']) \
.filter(problem_id=prob)
if score:
prev = subs.filter(submission__date__lte=time).count() - 1
penalty += prev * self.config['penalty'] * 60
else:
# We should always display the penalty, even if the user has a score of 0
prev = subs.count()
else:
prev = 0
if score:
cumtime = max(cumtime, dt)
format_data[str(prob)] = {'time': dt, 'points': score, 'penalty': prev}
points += score
participation.cumtime = cumtime + penalty
participation.score = points
participation.format_data = format_data
participation.save()
def display_user_problem(self, participation, contest_problem):
format_data = (participation.format_data or {}).get(str(contest_problem.id))
if format_data:
penalty = format_html('<small style="color:red"> ({penalty})</small>',
penalty=floatformat(format_data['penalty'])) if format_data['penalty'] else ''
return format_html(
'<td class="{state}"><a href="{url}">{points}{penalty}<div class="solving-time">{time}</div></a></td>',
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') +
self.best_solution_state(format_data['points'], contest_problem.points)),
url=reverse('contest_user_submissions',
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
points=floatformat(format_data['points']),
penalty=penalty,
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'),
)
else:
return mark_safe('<td></td>')

View file

@ -0,0 +1,91 @@
from abc import ABCMeta, abstractmethod, abstractproperty
from django.utils import six
class abstractclassmethod(classmethod):
__isabstractmethod__ = True
def __init__(self, callable):
callable.__isabstractmethod__ = True
super(abstractclassmethod, self).__init__(callable)
class BaseContestFormat(six.with_metaclass(ABCMeta)):
@abstractmethod
def __init__(self, contest, config):
self.config = config
self.contest = contest
@abstractproperty
def name(self):
"""
Name of this contest format. Should be invoked with gettext_lazy.
:return: str
"""
raise NotImplementedError()
@abstractclassmethod
def validate(cls, config):
"""
Validates the contest format configuration.
:param config: A dictionary containing the configuration for this contest format.
:return: None
:raises: ValidationError
"""
raise NotImplementedError()
@abstractmethod
def update_participation(self, participation):
"""
Updates a ContestParticipation object's score, cumtime, and format_data fields based on this contest format.
Implementations should call ContestParticipation.save().
:param participation: A ContestParticipation object.
:return: None
"""
raise NotImplementedError()
@abstractmethod
def display_user_problem(self, participation, contest_problem):
"""
Returns the HTML fragment to show a user's performance on an individual problem. This is expected to use
information from the format_data field instead of computing it from scratch.
:param participation: The ContestParticipation object linking the user to the contest.
:param contest_problem: The ContestProblem object representing the problem in question.
:return: An HTML fragment, marked as safe for Jinja2.
"""
raise NotImplementedError()
@abstractmethod
def display_participation_result(self, participation):
"""
Returns the HTML fragment to show a user's performance on the whole contest. This is expected to use
information from the format_data field instead of computing it from scratch.
:param participation: The ContestParticipation object.
:return: An HTML fragment, marked as safe for Jinja2.
"""
raise NotImplementedError()
@abstractmethod
def get_problem_breakdown(self, participation, contest_problems):
"""
Returns a machine-readable breakdown for the user's performance on every problem.
:param participation: The ContestParticipation object.
:param contest_problems: The list of ContestProblem objects to display performance for.
:return: A list of dictionaries, whose content is to be determined by the contest system.
"""
raise NotImplementedError()
@classmethod
def best_solution_state(cls, points, total):
if not points:
return 'failed-score'
if points == total:
return 'full-score'
return 'partial-score'

View file

@ -0,0 +1,70 @@
from datetime import timedelta
from django.core.exceptions import ValidationError
from django.db.models import Max
from django.template.defaultfilters import floatformat
from django.urls import reverse
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy
from judge.contest_format.base import BaseContestFormat
from judge.contest_format.registry import register_contest_format
from judge.utils.timedelta import nice_repr
@register_contest_format('default')
class DefaultContestFormat(BaseContestFormat):
name = gettext_lazy('Default')
@classmethod
def validate(cls, config):
if config is not None and (not isinstance(config, dict) or config):
raise ValidationError('default contest expects no config or empty dict as config')
def __init__(self, contest, config):
super(DefaultContestFormat, self).__init__(contest, config)
def update_participation(self, participation):
cumtime = 0
points = 0
format_data = {}
for result in participation.submissions.values('problem_id').annotate(
time=Max('submission__date'), points=Max('points'),
):
dt = (result['time'] - participation.start).total_seconds()
if result['points']:
cumtime += dt
format_data[str(result['problem_id'])] = {'time': dt, 'points': result['points']}
points += result['points']
participation.cumtime = max(cumtime, 0)
participation.score = points
participation.format_data = format_data
participation.save()
def display_user_problem(self, participation, contest_problem):
format_data = (participation.format_data or {}).get(str(contest_problem.id))
if format_data:
return format_html(
u'<td class="{state}"><a href="{url}">{points}<div class="solving-time">{time}</div></a></td>',
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') +
self.best_solution_state(format_data['points'], contest_problem.points)),
url=reverse('contest_user_submissions',
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
points=floatformat(format_data['points']),
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'),
)
else:
return mark_safe('<td></td>')
def display_participation_result(self, participation):
return format_html(
u'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
points=floatformat(participation.score),
cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday'),
)
def get_problem_breakdown(self, participation, contest_problems):
return [(participation.format_data or {}).get(str(contest_problem.id)) for contest_problem in contest_problems]

View file

@ -0,0 +1,122 @@
from datetime import timedelta
from django.core.exceptions import ValidationError
from django.db import connection
from django.template.defaultfilters import floatformat
from django.urls import reverse
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy
from judge.contest_format.default import DefaultContestFormat
from judge.contest_format.registry import register_contest_format
from judge.timezone import from_database_time
from judge.utils.timedelta import nice_repr
@register_contest_format('ecoo')
class ECOOContestFormat(DefaultContestFormat):
name = gettext_lazy('ECOO')
config_defaults = {'cumtime': False, 'first_ac_bonus': 10, 'time_bonus': 5}
config_validators = {'cumtime': lambda x: True, 'first_ac_bonus': lambda x: x >= 0, 'time_bonus': lambda x: x >= 0}
'''
cumtime: Specify True if cumulative time is to be used in breaking ties. Defaults to False.
first_ac_bonus: The number of points to award if a solution gets AC on its first non-IE/CE run. Defaults to 10.
time_bonus: Number of minutes to award an extra point for submitting before the contest end.
Specify 0 to disable. Defaults to 5.
'''
@classmethod
def validate(cls, config):
if config is None:
return
if not isinstance(config, dict):
raise ValidationError('ECOO-styled contest expects no config or dict as config')
for key, value in config.items():
if key not in cls.config_defaults:
raise ValidationError('unknown config key "%s"' % key)
if not isinstance(value, type(cls.config_defaults[key])):
raise ValidationError('invalid type for config key "%s"' % key)
if not cls.config_validators[key](value):
raise ValidationError('invalid value "%s" for config key "%s"' % (value, key))
def __init__(self, contest, config):
self.config = self.config_defaults.copy()
self.config.update(config or {})
self.contest = contest
def update_participation(self, participation):
cumtime = 0
points = 0
format_data = {}
with connection.cursor() as cursor:
cursor.execute('''
SELECT (
SELECT MAX(ccs.points)
FROM judge_contestsubmission ccs LEFT OUTER JOIN
judge_submission csub ON (csub.id = ccs.submission_id)
WHERE ccs.problem_id = cp.id AND ccs.participation_id = %s AND csub.date = MAX(sub.date)
) AS `score`, MAX(sub.date) AS `time`, cp.id AS `prob`, (
SELECT COUNT(ccs.id)
FROM judge_contestsubmission ccs LEFT OUTER JOIN
judge_submission csub ON (csub.id = ccs.submission_id)
WHERE ccs.problem_id = cp.id AND ccs.participation_id = %s AND csub.result NOT IN ('IE', 'CE')
) AS `subs`, cp.points AS `max_score`
FROM judge_contestproblem cp INNER JOIN
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
judge_submission sub ON (sub.id = cs.submission_id)
GROUP BY cp.id
''', (participation.id, participation.id, participation.id))
for score, time, prob, subs, max_score in cursor.fetchall():
time = from_database_time(time)
dt = (time - participation.start).total_seconds()
if self.config['cumtime']:
cumtime += dt
bonus = 0
if score > 0:
# First AC bonus
if subs == 1 and score == max_score:
bonus += self.config['first_ac_bonus']
# Time bonus
if self.config['time_bonus']:
bonus += (participation.end_time - time).total_seconds() // 60 // self.config['time_bonus']
points += bonus
format_data[str(prob)] = {'time': dt, 'points': score, 'bonus': bonus}
points += score
participation.cumtime = cumtime
participation.score = points
participation.format_data = format_data
participation.save()
def display_user_problem(self, participation, contest_problem):
format_data = (participation.format_data or {}).get(str(contest_problem.id))
if format_data:
bonus = format_html('<small> +{bonus}</small>',
bonus=floatformat(format_data['bonus'])) if format_data['bonus'] else ''
return format_html(
'<td class="{state}"><a href="{url}">{points}{bonus}<div class="solving-time">{time}</div></a></td>',
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') +
self.best_solution_state(format_data['points'], contest_problem.points)),
url=reverse('contest_user_submissions',
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
points=floatformat(format_data['points']),
bonus=bonus,
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'),
)
else:
return mark_safe('<td></td>')
def display_participation_result(self, participation):
return format_html(
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
points=floatformat(participation.score),
cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday') if self.config['cumtime'] else '',
)

View file

@ -0,0 +1,99 @@
from datetime import timedelta
from django.core.exceptions import ValidationError
from django.db import connection
from django.template.defaultfilters import floatformat
from django.urls import reverse
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy
from judge.contest_format.default import DefaultContestFormat
from judge.contest_format.registry import register_contest_format
from judge.timezone import from_database_time
from judge.utils.timedelta import nice_repr
@register_contest_format('ioi')
class IOIContestFormat(DefaultContestFormat):
name = gettext_lazy('IOI')
config_defaults = {'cumtime': False}
'''
cumtime: Specify True if time penalties are to be computed. Defaults to False.
'''
@classmethod
def validate(cls, config):
if config is None:
return
if not isinstance(config, dict):
raise ValidationError('IOI-styled contest expects no config or dict as config')
for key, value in config.items():
if key not in cls.config_defaults:
raise ValidationError('unknown config key "%s"' % key)
if not isinstance(value, type(cls.config_defaults[key])):
raise ValidationError('invalid type for config key "%s"' % key)
def __init__(self, contest, config):
self.config = self.config_defaults.copy()
self.config.update(config or {})
self.contest = contest
def update_participation(self, participation):
cumtime = 0
points = 0
format_data = {}
with connection.cursor() as cursor:
cursor.execute('''
SELECT MAX(cs.points) as `score`, (
SELECT MIN(csub.date)
FROM judge_contestsubmission ccs LEFT OUTER JOIN
judge_submission csub ON (csub.id = ccs.submission_id)
WHERE ccs.problem_id = cp.id AND ccs.participation_id = %s AND ccs.points = MAX(cs.points)
) AS `time`, cp.id AS `prob`
FROM judge_contestproblem cp INNER JOIN
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
judge_submission sub ON (sub.id = cs.submission_id)
GROUP BY cp.id
''', (participation.id, participation.id))
for score, time, prob in cursor.fetchall():
if self.config['cumtime']:
dt = (from_database_time(time) - participation.start).total_seconds()
if score:
cumtime += dt
else:
dt = 0
format_data[str(prob)] = {'time': dt, 'points': score}
points += score
participation.cumtime = max(cumtime, 0)
participation.score = points
participation.format_data = format_data
participation.save()
def display_user_problem(self, participation, contest_problem):
format_data = (participation.format_data or {}).get(str(contest_problem.id))
if format_data:
return format_html(
'<td class="{state}"><a href="{url}">{points}<div class="solving-time">{time}</div></a></td>',
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') +
self.best_solution_state(format_data['points'], contest_problem.points)),
url=reverse('contest_user_submissions',
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
points=floatformat(format_data['points']),
time=nice_repr(timedelta(seconds=format_data['time']), 'noday') if self.config['cumtime'] else '',
)
else:
return mark_safe('<td></td>')
def display_participation_result(self, participation):
return format_html(
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
points=floatformat(participation.score),
cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday') if self.config['cumtime'] else '',
)

View file

@ -0,0 +1,16 @@
from django.utils import six
formats = {}
def register_contest_format(name):
def register_class(contest_format_class):
assert name not in formats
formats[name] = contest_format_class
return contest_format_class
return register_class
def choices():
return [(key, value.name) for key, value in sorted(six.iteritems(formats))]

23
judge/dblock.py Normal file
View file

@ -0,0 +1,23 @@
from itertools import chain
from django.db import connection, transaction
class LockModel(object):
def __init__(self, write, read=()):
self.tables = ', '.join(chain(
('`%s` WRITE' % model._meta.db_table for model in write),
('`%s` READ' % model._meta.db_table for model in read),
))
self.cursor = connection.cursor()
def __enter__(self):
self.cursor.execute('LOCK TABLES ' + self.tables)
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
transaction.commit()
else:
transaction.rollback()
self.cursor.execute('UNLOCK TABLES')
self.cursor.close()

18
judge/event_poster.py Normal file
View file

@ -0,0 +1,18 @@
from django.conf import settings
__all__ = ['last', 'post']
if not settings.EVENT_DAEMON_USE:
real = False
def post(channel, message):
return 0
def last():
return 0
elif hasattr(settings, 'EVENT_DAEMON_AMQP'):
from .event_poster_amqp import last, post
real = True
else:
from .event_poster_ws import last, post
real = True

View file

@ -0,0 +1,55 @@
import json
import threading
from time import time
import pika
from django.conf import settings
from pika.exceptions import AMQPError
__all__ = ['EventPoster', 'post', 'last']
class EventPoster(object):
def __init__(self):
self._connect()
self._exchange = settings.EVENT_DAEMON_AMQP_EXCHANGE
def _connect(self):
self._conn = pika.BlockingConnection(pika.URLParameters(settings.EVENT_DAEMON_AMQP))
self._chan = self._conn.channel()
def post(self, channel, message, tries=0):
try:
id = int(time() * 1000000)
self._chan.basic_publish(self._exchange, '',
json.dumps({'id': id, 'channel': channel, 'message': message}))
return id
except AMQPError:
if tries > 10:
raise
self._connect()
return self.post(channel, message, tries + 1)
_local = threading.local()
def _get_poster():
if 'poster' not in _local.__dict__:
_local.poster = EventPoster()
return _local.poster
def post(channel, message):
try:
return _get_poster().post(channel, message)
except AMQPError:
try:
del _local.poster
except AttributeError:
pass
return 0
def last():
return int(time() * 1000000)

82
judge/event_poster_ws.py Normal file
View file

@ -0,0 +1,82 @@
import json
import socket
import threading
from django.conf import settings
from websocket import WebSocketException, create_connection
__all__ = ['EventPostingError', 'EventPoster', 'post', 'last']
_local = threading.local()
class EventPostingError(RuntimeError):
pass
class EventPoster(object):
def __init__(self):
self._connect()
def _connect(self):
self._conn = create_connection(settings.EVENT_DAEMON_POST)
if settings.EVENT_DAEMON_KEY is not None:
self._conn.send(json.dumps({'command': 'auth', 'key': settings.EVENT_DAEMON_KEY}))
resp = json.loads(self._conn.recv())
if resp['status'] == 'error':
raise EventPostingError(resp['code'])
def post(self, channel, message, tries=0):
try:
self._conn.send(json.dumps({'command': 'post', 'channel': channel, 'message': message}))
resp = json.loads(self._conn.recv())
if resp['status'] == 'error':
raise EventPostingError(resp['code'])
else:
return resp['id']
except WebSocketException:
if tries > 10:
raise
self._connect()
return self.post(channel, message, tries + 1)
def last(self, tries=0):
try:
self._conn.send('{"command": "last-msg"}')
resp = json.loads(self._conn.recv())
if resp['status'] == 'error':
raise EventPostingError(resp['code'])
else:
return resp['id']
except WebSocketException:
if tries > 10:
raise
self._connect()
return self.last(tries + 1)
def _get_poster():
if 'poster' not in _local.__dict__:
_local.poster = EventPoster()
return _local.poster
def post(channel, message):
try:
return _get_poster().post(channel, message)
except (WebSocketException, socket.error):
try:
del _local.poster
except AttributeError:
pass
return 0
def last():
try:
return _get_poster().last()
except (WebSocketException, socket.error):
try:
del _local.poster
except AttributeError:
pass
return 0

99
judge/feed.py Normal file
View file

@ -0,0 +1,99 @@
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.contrib.syndication.views import Feed
from django.core.cache import cache
from django.utils import timezone
from django.utils.feedgenerator import Atom1Feed
from judge.jinja2.markdown import markdown
from judge.models import BlogPost, Comment, Problem
class ProblemFeed(Feed):
title = 'Recently Added %s Problems' % settings.SITE_NAME
link = '/'
description = 'The latest problems added on the %s website' % settings.SITE_LONG_NAME
def items(self):
return Problem.objects.filter(is_public=True, is_organization_private=False).order_by('-date', '-id')[:25]
def item_title(self, problem):
return problem.name
def item_description(self, problem):
key = 'problem_feed:%d' % problem.id
desc = cache.get(key)
if desc is None:
desc = str(markdown(problem.description, 'problem'))[:500] + '...'
cache.set(key, desc, 86400)
return desc
def item_pubdate(self, problem):
return problem.date
item_updateddate = item_pubdate
class AtomProblemFeed(ProblemFeed):
feed_type = Atom1Feed
subtitle = ProblemFeed.description
class CommentFeed(Feed):
title = 'Latest %s Comments' % settings.SITE_NAME
link = '/'
description = 'The latest comments on the %s website' % settings.SITE_LONG_NAME
def items(self):
return Comment.most_recent(AnonymousUser(), 25)
def item_title(self, comment):
return '%s -> %s' % (comment.author.user.username, comment.page_title)
def item_description(self, comment):
key = 'comment_feed:%d' % comment.id
desc = cache.get(key)
if desc is None:
desc = str(markdown(comment.body, 'comment'))
cache.set(key, desc, 86400)
return desc
def item_pubdate(self, comment):
return comment.time
item_updateddate = item_pubdate
class AtomCommentFeed(CommentFeed):
feed_type = Atom1Feed
subtitle = CommentFeed.description
class BlogFeed(Feed):
title = 'Latest %s Blog Posts' % settings.SITE_NAME
link = '/'
description = 'The latest blog posts from the %s' % settings.SITE_LONG_NAME
def items(self):
return BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now()).order_by('-sticky', '-publish_on')
def item_title(self, post):
return post.title
def item_description(self, post):
key = 'blog_feed:%d' % post.id
summary = cache.get(key)
if summary is None:
summary = str(markdown(post.summary or post.content, 'blog'))
cache.set(key, summary, 86400)
return summary
def item_pubdate(self, post):
return post.publish_on
item_updateddate = item_pubdate
class AtomBlogFeed(BlogFeed):
feed_type = Atom1Feed
subtitle = BlogFeed.description

173
judge/fixtures/demo.json Normal file
View file

@ -0,0 +1,173 @@
[
{
"fields": {
"about": "",
"ace_theme": "github",
"current_contest": null,
"display_rank": "admin",
"ip": "10.0.2.2",
"language": 1,
"last_access": "2017-12-02T08:57:10.093Z",
"math_engine": "auto",
"mute": false,
"organizations": [
1
],
"performance_points": 0.0,
"points": 0.0,
"problem_count": 0,
"rating": null,
"timezone": "America/Toronto",
"user": 1,
"user_script": ""
},
"model": "judge.profile",
"pk": 1
},
{
"fields": {
"date_joined": "2017-12-02T08:34:17.408Z",
"email": "",
"first_name": "",
"groups": [
],
"is_active": true,
"is_staff": true,
"is_superuser": true,
"last_login": "2017-12-02T08:34:31.840Z",
"last_name": "",
"password": "pbkdf2_sha256$36000$eFRRZq4DgktS$md1gk0bBJb7PH/+3YEXkcCW8K+KmiI+y/amqR32G2DY=",
"user_permissions": [
],
"username": "admin"
},
"model": "auth.user",
"pk": 1
},
{
"fields": {
"about": "This is a sample organization. You can use organizations to split up your user base, host private contests, and more.",
"access_code": null,
"admins": [
1
],
"creation_date": "2017-12-02T08:50:25.199Z",
"is_open": true,
"slug": "dmoj",
"name": "DMOJ: Modern Online Judge",
"registrant": 1,
"short_name": "DMOJ",
"slots": null
},
"model": "judge.organization",
"pk": 1
},
{
"fields": {
"full_name": "Simple Math",
"name": "Simple Math"
},
"model": "judge.problemtype",
"pk": 1
},
{
"fields": {
"full_name": "Uncategorized",
"name": "Uncategorized"
},
"model": "judge.problemgroup",
"pk": 1
},
{
"fields": {
"ac_rate": 0.0,
"allowed_languages": [
3,
4,
5,
6,
2,
7,
1,
8
],
"authors": [
1
],
"banned_users": [
],
"code": "aplusb",
"curators": [
],
"date": "2017-12-02T05:00:00Z",
"description": "Tudor is sitting in math class, on his laptop. Clearly, he is not paying attention in this situation. However, he gets called on by his math teacher to do some problems. Since his math teacher did not expect much from Tudor, he only needs to do some simple addition problems. However, simple for you and I may not be simple for Tudor , so please help him!\n\n## Input Specification\n\nThe first line will contain an integer ~N~ (~1 \\le N \\le 100\\,000~), the number of addition problems Tudor needs to do. The next ~N~ lines will each contain two space-separated integers whose absolute value is less than ~1\\,000\\,000\\,000~, the two integers Tudor needs to add.\n\n## Output Specification\n\nOutput ~N~ lines of one integer each, the solutions to the addition problems in order.\n\n## Sample Input\n\n 2\n 1 1\n -1 0\n\n## Sample Output\n\n 2\n -1",
"group": 1,
"is_manually_managed": false,
"is_public": true,
"license": null,
"memory_limit": 65536,
"name": "A Plus B",
"og_image": "",
"partial": true,
"points": 5.0,
"short_circuit": false,
"summary": "",
"testers": [
],
"time_limit": 2.0,
"types": [
1
],
"user_count": 0
},
"model": "judge.problem",
"pk": 1
},
{
"fields": {
"authors": [
1
],
"content": "Welcome to DMOJ!\n\n```python\nprint \"Hello, World!\"\n```\n\nYou can get started by checking out [this problem we've added for you](/problem/aplusb).",
"og_image": "",
"publish_on": "2017-12-02T05:00:00Z",
"slug": "first-post",
"sticky": true,
"summary": "",
"title": "First Post",
"visible": true
},
"model": "judge.blogpost",
"pk": 1
},
{
"fields": {
"author": 1,
"body": "This is your first comment!",
"hidden": false,
"level": 0,
"lft": 1,
"page": "b:1",
"parent": null,
"rght": 2,
"score": 0,
"time": "2017-12-02T08:46:54.007Z",
"tree_id": 1
},
"model": "judge.comment",
"pk": 1
},
{
"fields": {
"domain": "localhost:8081",
"name": "DMOJ: Modern Online Judge"
},
"model": "sites.site",
"pk": 1
}
]

View file

@ -0,0 +1,122 @@
[
{
"fields": {
"ace": "python",
"common_name": "Python",
"description": "",
"extension": "",
"info": "python 2.7.9",
"key": "PY2",
"name": "Python 2",
"pygments": "python",
"short_name": ""
},
"model": "judge.language",
"pk": 1
},
{
"fields": {
"ace": "assembly_x86",
"common_name": "Assembly",
"description": "",
"extension": "",
"info": "binutils 2.25",
"key": "GAS64",
"name": "Assembly (x64)",
"pygments": "gas",
"short_name": ""
},
"model": "judge.language",
"pk": 2
},
{
"fields": {
"ace": "AWK",
"common_name": "Awk",
"description": "",
"extension": "",
"info": "mawk 1.3.3",
"key": "AWK",
"name": "AWK",
"pygments": "awk",
"short_name": "AWK"
},
"model": "judge.language",
"pk": 3
},
{
"fields": {
"ace": "c_cpp",
"common_name": "C",
"description": "Compile options: `gcc -std=c99 -Wall -O2 -lm -march=native -s`\r\n",
"extension": "",
"info": "gcc 4.9.2",
"key": "C",
"name": "C",
"pygments": "c",
"short_name": ""
},
"model": "judge.language",
"pk": 4
},
{
"fields": {
"ace": "c_cpp",
"common_name": "C++",
"description": "Compile options: `g++ -Wall -O2 -lm -march=native -s`\r\n",
"extension": "",
"info": "g++ 4.9.2",
"key": "CPP03",
"name": "C++03",
"pygments": "cpp",
"short_name": "C++03"
},
"model": "judge.language",
"pk": 5
},
{
"fields": {
"ace": "c_cpp",
"common_name": "C++",
"description": "Compile options: `g++ -std=c++11 -Wall -O2 -lm -march=native -s`\r\n",
"extension": "",
"info": "g++-4.9.2 -std=c++11",
"key": "CPP11",
"name": "C++11",
"pygments": "cpp",
"short_name": "C++11"
},
"model": "judge.language",
"pk": 6
},
{
"fields": {
"ace": "perl",
"common_name": "Perl",
"description": "",
"extension": "",
"info": "perl 5.10.1",
"key": "PERL",
"name": "Perl",
"pygments": "perl",
"short_name": ""
},
"model": "judge.language",
"pk": 7
},
{
"fields": {
"ace": "python",
"common_name": "Python",
"description": "",
"extension": "",
"info": "python 3.4.2",
"key": "PY3",
"name": "Python 3",
"pygments": "python3",
"short_name": ""
},
"model": "judge.language",
"pk": 8
}
]

View file

@ -0,0 +1,98 @@
[
{
"fields": {
"key": "problems",
"label": "Problems",
"level": 0,
"lft": 1,
"order": 1,
"parent": null,
"path": "/problems/",
"regex": "^/problem",
"rght": 2,
"tree_id": 1
},
"model": "judge.navigationbar",
"pk": 1
},
{
"fields": {
"key": "submit",
"label": "Submissions",
"level": 0,
"lft": 1,
"order": 2,
"parent": null,
"path": "/submissions/",
"regex": "^/submi|^/src/",
"rght": 2,
"tree_id": 2
},
"model": "judge.navigationbar",
"pk": 2
},
{
"fields": {
"key": "user",
"label": "Users",
"level": 0,
"lft": 1,
"order": 3,
"parent": null,
"path": "/users/",
"regex": "^/user",
"rght": 2,
"tree_id": 3
},
"model": "judge.navigationbar",
"pk": 3
},
{
"fields": {
"key": "contest",
"label": "Contests",
"level": 0,
"lft": 1,
"order": 5,
"parent": null,
"path": "/contests/",
"regex": "^/contest",
"rght": 2,
"tree_id": 4
},
"model": "judge.navigationbar",
"pk": 5
},
{
"fields": {
"key": "about",
"label": "About",
"level": 0,
"lft": 1,
"order": 6,
"parent": null,
"path": "/about/",
"regex": "^/about/$",
"rght": 4,
"tree_id": 5
},
"model": "judge.navigationbar",
"pk": 6
},
{
"fields": {
"key": "status",
"label": "Status",
"level": 1,
"lft": 2,
"order": 7,
"parent": 6,
"path": "/status/",
"regex": "^/status/$|^/judge/",
"rght": 3,
"tree_id": 5
},
"model": "judge.navigationbar",
"pk": 7
}
]

159
judge/forms.py Normal file
View file

@ -0,0 +1,159 @@
from operator import attrgetter
import pyotp
from django import forms
from django.conf import settings
from django.contrib.auth.forms import AuthenticationForm
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.db.models import Q
from django.forms import CharField, Form, ModelForm
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django_ace import AceWidget
from judge.models import Contest, Language, Organization, PrivateMessage, Problem, Profile, Submission
from judge.utils.subscription import newsletter_id
from judge.widgets import HeavyPreviewPageDownWidget, MathJaxPagedownWidget, PagedownWidget, Select2MultipleWidget, \
Select2Widget
def fix_unicode(string, unsafe=tuple('\u202a\u202b\u202d\u202e')):
return string + (sum(k in unsafe for k in string) - string.count('\u202c')) * '\u202c'
class ProfileForm(ModelForm):
if newsletter_id is not None:
newsletter = forms.BooleanField(label=_('Subscribe to contest updates'), initial=False, required=False)
test_site = forms.BooleanField(label=_('Enable experimental features'), initial=False, required=False)
class Meta:
model = Profile
fields = ['about', 'organizations', 'timezone', 'language', 'ace_theme', 'user_script']
widgets = {
'user_script': AceWidget(theme='github'),
'timezone': Select2Widget(attrs={'style': 'width:200px'}),
'language': Select2Widget(attrs={'style': 'width:200px'}),
'ace_theme': Select2Widget(attrs={'style': 'width:200px'}),
}
has_math_config = bool(settings.MATHOID_URL)
if has_math_config:
fields.append('math_engine')
widgets['math_engine'] = Select2Widget(attrs={'style': 'width:200px'})
if HeavyPreviewPageDownWidget is not None:
widgets['about'] = HeavyPreviewPageDownWidget(
preview=reverse_lazy('profile_preview'),
attrs={'style': 'max-width:700px;min-width:700px;width:700px'},
)
def clean(self):
organizations = self.cleaned_data.get('organizations') or []
max_orgs = settings.DMOJ_USER_MAX_ORGANIZATION_COUNT
if sum(org.is_open for org in organizations) > max_orgs:
raise ValidationError(
_('You may not be part of more than {count} public organizations.').format(count=max_orgs))
return self.cleaned_data
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super(ProfileForm, self).__init__(*args, **kwargs)
if not user.has_perm('judge.edit_all_organization'):
self.fields['organizations'].queryset = Organization.objects.filter(
Q(is_open=True) | Q(id__in=user.profile.organizations.all()),
)
class ProblemSubmitForm(ModelForm):
source = CharField(max_length=65536, widget=AceWidget(theme='twilight', no_ace_media=True))
def __init__(self, *args, **kwargs):
super(ProblemSubmitForm, self).__init__(*args, **kwargs)
self.fields['problem'].empty_label = None
self.fields['problem'].widget = forms.HiddenInput()
self.fields['language'].empty_label = None
self.fields['language'].label_from_instance = attrgetter('display_name')
self.fields['language'].queryset = Language.objects.filter(judges__online=True).distinct()
class Meta:
model = Submission
fields = ['problem', 'language']
class EditOrganizationForm(ModelForm):
class Meta:
model = Organization
fields = ['about', 'logo_override_image', 'admins']
widgets = {'admins': Select2MultipleWidget()}
if HeavyPreviewPageDownWidget is not None:
widgets['about'] = HeavyPreviewPageDownWidget(preview=reverse_lazy('organization_preview'))
class NewMessageForm(ModelForm):
class Meta:
model = PrivateMessage
fields = ['title', 'content']
widgets = {}
if PagedownWidget is not None:
widgets['content'] = MathJaxPagedownWidget()
class CustomAuthenticationForm(AuthenticationForm):
def __init__(self, *args, **kwargs):
super(CustomAuthenticationForm, self).__init__(*args, **kwargs)
self.fields['username'].widget.attrs.update({'placeholder': _('Username')})
self.fields['password'].widget.attrs.update({'placeholder': _('Password')})
self.has_google_auth = self._has_social_auth('GOOGLE_OAUTH2')
self.has_facebook_auth = self._has_social_auth('FACEBOOK')
self.has_github_auth = self._has_social_auth('GITHUB_SECURE')
def _has_social_auth(self, key):
return (getattr(settings, 'SOCIAL_AUTH_%s_KEY' % key, None) and
getattr(settings, 'SOCIAL_AUTH_%s_SECRET' % key, None))
class NoAutoCompleteCharField(forms.CharField):
def widget_attrs(self, widget):
attrs = super(NoAutoCompleteCharField, self).widget_attrs(widget)
attrs['autocomplete'] = 'off'
return attrs
class TOTPForm(Form):
TOLERANCE = settings.DMOJ_TOTP_TOLERANCE_HALF_MINUTES
totp_token = NoAutoCompleteCharField(validators=[
RegexValidator('^[0-9]{6}$', _('Two Factor Authentication tokens must be 6 decimal digits.')),
])
def __init__(self, *args, **kwargs):
self.totp_key = kwargs.pop('totp_key')
super(TOTPForm, self).__init__(*args, **kwargs)
def clean_totp_token(self):
if not pyotp.TOTP(self.totp_key).verify(self.cleaned_data['totp_token'], valid_window=self.TOLERANCE):
raise ValidationError(_('Invalid Two Factor Authentication token.'))
class ProblemCloneForm(Form):
code = CharField(max_length=20, validators=[RegexValidator('^[a-z0-9]+$', _('Problem code must be ^[a-z0-9]+$'))])
def clean_code(self):
code = self.cleaned_data['code']
if Problem.objects.filter(code=code).exists():
raise ValidationError(_('Problem with code already exists.'))
return code
class ContestCloneForm(Form):
key = CharField(max_length=20, validators=[RegexValidator('^[a-z0-9]+$', _('Contest id must be ^[a-z0-9]+$'))])
def clean_key(self):
key = self.cleaned_data['key']
if Contest.objects.filter(key=key).exists():
raise ValidationError(_('Contest with key already exists.'))
return key

55
judge/fulltext.py Normal file
View file

@ -0,0 +1,55 @@
# From: http://www.mercurytide.co.uk/news/article/django-full-text-search/
from django.db import connection, models
from django.db.models.query import QuerySet
class SearchQuerySet(QuerySet):
DEFAULT = ''
BOOLEAN = ' IN BOOLEAN MODE'
NATURAL_LANGUAGE = ' IN NATURAL LANGUAGE MODE'
QUERY_EXPANSION = ' WITH QUERY EXPANSION'
def __init__(self, fields=None, **kwargs):
super(SearchQuerySet, self).__init__(**kwargs)
self._search_fields = fields
def _clone(self, *args, **kwargs):
queryset = super(SearchQuerySet, self)._clone(*args, **kwargs)
queryset._search_fields = self._search_fields
return queryset
def search(self, query, mode=DEFAULT):
meta = self.model._meta
# Get the table name and column names from the model
# in `table_name`.`column_name` style
columns = [meta.get_field(name).column for name in self._search_fields]
full_names = ['%s.%s' %
(connection.ops.quote_name(meta.db_table),
connection.ops.quote_name(column))
for column in columns]
# Create the MATCH...AGAINST expressions
fulltext_columns = ', '.join(full_names)
match_expr = ('MATCH(%s) AGAINST (%%s%s)' % (fulltext_columns, mode))
# Add the extra SELECT and WHERE options
return self.extra(select={'relevance': match_expr},
select_params=[query],
where=[match_expr],
params=[query])
class SearchManager(models.Manager):
def __init__(self, fields=None):
super(SearchManager, self).__init__()
self._search_fields = fields
def get_queryset(self):
if self._search_fields is not None:
return SearchQuerySet(model=self.model, fields=self._search_fields)
return super(SearchManager, self).get_queryset()
def search(self, *args, **kwargs):
return self.get_queryset().search(*args, **kwargs)

37
judge/highlight_code.py Normal file
View file

@ -0,0 +1,37 @@
from django.utils.html import escape, mark_safe
__all__ = ['highlight_code']
def _make_pre_code(code):
return mark_safe('<pre>' + escape(code) + '</pre>')
def _wrap_code(inner):
yield 0, "<code>"
for tup in inner:
yield tup
yield 0, "</code>"
try:
import pygments
import pygments.lexers
import pygments.formatters.html
import pygments.util
except ImportError:
def highlight_code(code, language, cssclass=None):
return _make_pre_code(code)
else:
class HtmlCodeFormatter(pygments.formatters.HtmlFormatter):
def wrap(self, source, outfile):
return self._wrap_div(self._wrap_pre(_wrap_code(source)))
def highlight_code(code, language, cssclass='codehilite'):
try:
lexer = pygments.lexers.get_lexer_by_name(language)
except pygments.util.ClassNotFound:
return _make_pre_code(code)
# return mark_safe(pygments.highlight(code, lexer, HtmlCodeFormatter(cssclass=cssclass, linenos='table')))
return mark_safe(pygments.highlight(code, lexer, HtmlCodeFormatter(cssclass=cssclass)))

36
judge/jinja2/__init__.py Normal file
View file

@ -0,0 +1,36 @@
import itertools
import json
from django.utils.http import urlquote
from jinja2.ext import Extension
from mptt.utils import get_cached_trees
from statici18n.templatetags.statici18n import inlinei18n
from judge.highlight_code import highlight_code
from judge.user_translations import gettext
from . import (camo, datetime, filesize, gravatar, language, markdown, rating, reference, render, social,
spaceless, submission, timedelta)
from . import registry
registry.function('str', str)
registry.filter('str', str)
registry.filter('json', json.dumps)
registry.filter('highlight', highlight_code)
registry.filter('urlquote', urlquote)
registry.filter('roundfloat', round)
registry.function('inlinei18n', inlinei18n)
registry.function('mptt_tree', get_cached_trees)
registry.function('user_trans', gettext)
@registry.function
def counter(start=1):
return itertools.count(start).__next__
class DMOJExtension(Extension):
def __init__(self, env):
super(DMOJExtension, self).__init__(env)
env.globals.update(registry.globals)
env.filters.update(registry.filters)
env.tests.update(registry.tests)

9
judge/jinja2/camo.py Normal file
View file

@ -0,0 +1,9 @@
from judge.utils.camo import client as camo_client
from . import registry
@registry.filter
def camo(url):
if camo_client is None:
return url
return camo_client.rewrite_url(url)

27
judge/jinja2/datetime.py Normal file
View file

@ -0,0 +1,27 @@
import functools
from django.template.defaultfilters import date, time
from django.templatetags.tz import localtime
from django.utils.translation import gettext as _
from . import registry
def localtime_wrapper(func):
@functools.wraps(func)
def wrapper(datetime, *args, **kwargs):
if getattr(datetime, 'convert_to_local_time', True):
datetime = localtime(datetime)
return func(datetime, *args, **kwargs)
return wrapper
registry.filter(localtime_wrapper(date))
registry.filter(localtime_wrapper(time))
@registry.function
@registry.render_with('widgets/relative-time.html')
def relative_time(time, format=_('N j, Y, g:i a'), rel=_('{time}'), abs=_('on {time}')):
return {'time': time, 'format': format, 'rel_format': rel, 'abs_format': abs}

36
judge/jinja2/filesize.py Normal file
View file

@ -0,0 +1,36 @@
from django.utils.html import avoid_wrapping
from . import registry
def _format_size(bytes, callback):
bytes = float(bytes)
KB = 1 << 10
MB = 1 << 20
GB = 1 << 30
TB = 1 << 40
PB = 1 << 50
if bytes < KB:
return callback('', bytes)
elif bytes < MB:
return callback('K', bytes / KB)
elif bytes < GB:
return callback('M', bytes / MB)
elif bytes < TB:
return callback('G', bytes / GB)
elif bytes < PB:
return callback('T', bytes / TB)
else:
return callback('P', bytes / PB)
@registry.filter
def kbdetailformat(bytes):
return avoid_wrapping(_format_size(bytes * 1024, lambda x, y: ['%d %sB', '%.2f %sB'][bool(x)] % (y, x)))
@registry.filter
def kbsimpleformat(kb):
return _format_size(kb * 1024, lambda x, y: '%.0f%s' % (y, x or 'B'))

25
judge/jinja2/gravatar.py Normal file
View file

@ -0,0 +1,25 @@
import hashlib
from django.contrib.auth.models import AbstractUser
from django.utils.http import urlencode
from judge.models import Profile
from judge.utils.unicode import utf8bytes
from . import registry
@registry.function
def gravatar(email, size=80, default=None):
if isinstance(email, Profile):
if default is None:
default = email.mute
email = email.user.email
elif isinstance(email, AbstractUser):
email = email.email
gravatar_url = '//www.gravatar.com/avatar/' + hashlib.md5(utf8bytes(email.strip().lower())).hexdigest() + '?'
args = {'d': 'identicon', 's': str(size)}
if default:
args['f'] = 'y'
gravatar_url += urlencode(args)
return gravatar_url

18
judge/jinja2/language.py Normal file
View file

@ -0,0 +1,18 @@
from django.utils import translation
from . import registry
@registry.function('language_info')
def get_language_info(language):
# ``language`` is either a language code string or a sequence
# with the language code as its first item
if len(language[0]) > 1:
return translation.get_language_info(language[0])
else:
return translation.get_language_info(str(language))
@registry.function('language_info_list')
def get_language_info_list(langs):
return [get_language_info(lang) for lang in langs]

View file

@ -0,0 +1,142 @@
import logging
import re
from html.parser import HTMLParser
from urllib.parse import urlparse
import mistune
from django.conf import settings
from jinja2 import Markup
from lxml import html
from lxml.etree import ParserError, XMLSyntaxError
from judge.highlight_code import highlight_code
from judge.jinja2.markdown.lazy_load import lazy_load as lazy_load_processor
from judge.jinja2.markdown.math import MathInlineGrammar, MathInlineLexer, MathRenderer
from judge.utils.camo import client as camo_client
from judge.utils.texoid import TEXOID_ENABLED, TexoidRenderer
from .. import registry
logger = logging.getLogger('judge.html')
NOFOLLOW_WHITELIST = settings.NOFOLLOW_EXCLUDED
class CodeSafeInlineGrammar(mistune.InlineGrammar):
double_emphasis = re.compile(r'^\*{2}([\s\S]+?)()\*{2}(?!\*)') # **word**
emphasis = re.compile(r'^\*((?:\*\*|[^\*])+?)()\*(?!\*)') # *word*
class AwesomeInlineGrammar(MathInlineGrammar, CodeSafeInlineGrammar):
pass
class AwesomeInlineLexer(MathInlineLexer, mistune.InlineLexer):
grammar_class = AwesomeInlineGrammar
class AwesomeRenderer(MathRenderer, mistune.Renderer):
def __init__(self, *args, **kwargs):
self.nofollow = kwargs.pop('nofollow', True)
self.texoid = TexoidRenderer() if kwargs.pop('texoid', False) else None
self.parser = HTMLParser()
super(AwesomeRenderer, self).__init__(*args, **kwargs)
def _link_rel(self, href):
if href:
try:
url = urlparse(href)
except ValueError:
return ' rel="nofollow"'
else:
if url.netloc and url.netloc not in NOFOLLOW_WHITELIST:
return ' rel="nofollow"'
return ''
def autolink(self, link, is_email=False):
text = link = mistune.escape(link)
if is_email:
link = 'mailto:%s' % link
return '<a href="%s"%s>%s</a>' % (link, self._link_rel(link), text)
def table(self, header, body):
return (
'<table class="table">\n<thead>%s</thead>\n'
'<tbody>\n%s</tbody>\n</table>\n'
) % (header, body)
def link(self, link, title, text):
link = mistune.escape_link(link)
if not title:
return '<a href="%s"%s>%s</a>' % (link, self._link_rel(link), text)
title = mistune.escape(title, quote=True)
return '<a href="%s" title="%s"%s>%s</a>' % (link, title, self._link_rel(link), text)
def block_code(self, code, lang=None):
if not lang:
return '\n<pre><code>%s</code></pre>\n' % mistune.escape(code).rstrip()
return highlight_code(code, lang)
def block_html(self, html):
if self.texoid and html.startswith('<latex'):
attr = html[6:html.index('>')]
latex = html[html.index('>') + 1:html.rindex('<')]
latex = self.parser.unescape(latex)
result = self.texoid.get_result(latex)
if not result:
return '<pre>%s</pre>' % mistune.escape(latex, smart_amp=False)
elif 'error' not in result:
img = ('''<img src="%(svg)s" onerror="this.src='%(png)s';this.onerror=null"'''
'width="%(width)s" height="%(height)s"%(tail)s>') % {
'svg': result['svg'], 'png': result['png'],
'width': result['meta']['width'], 'height': result['meta']['height'],
'tail': ' /' if self.options.get('use_xhtml') else '',
}
style = ['max-width: 100%',
'height: %s' % result['meta']['height'],
'max-height: %s' % result['meta']['height'],
'width: %s' % result['meta']['height']]
if 'inline' in attr:
tag = 'span'
else:
tag = 'div'
style += ['text-align: center']
return '<%s style="%s">%s</%s>' % (tag, ';'.join(style), img, tag)
else:
return '<pre>%s</pre>' % mistune.escape(result['error'], smart_amp=False)
return super(AwesomeRenderer, self).block_html(html)
def header(self, text, level, *args, **kwargs):
return super(AwesomeRenderer, self).header(text, level + 2, *args, **kwargs)
@registry.filter
def markdown(value, style, math_engine=None, lazy_load=False):
styles = settings.MARKDOWN_STYLES.get(style, settings.MARKDOWN_DEFAULT_STYLE)
escape = styles.get('safe_mode', True)
nofollow = styles.get('nofollow', True)
texoid = TEXOID_ENABLED and styles.get('texoid', False)
math = hasattr(settings, 'MATHOID_URL') and styles.get('math', False)
post_processors = []
if styles.get('use_camo', False) and camo_client is not None:
post_processors.append(camo_client.update_tree)
if lazy_load:
post_processors.append(lazy_load_processor)
renderer = AwesomeRenderer(escape=escape, nofollow=nofollow, texoid=texoid,
math=math and math_engine is not None, math_engine=math_engine)
markdown = mistune.Markdown(renderer=renderer, inline=AwesomeInlineLexer,
parse_block_html=1, parse_inline_html=1)
result = markdown(value)
if post_processors:
try:
tree = html.fromstring(result, parser=html.HTMLParser(recover=True))
except (XMLSyntaxError, ParserError) as e:
if result and (not isinstance(e, ParserError) or e.args[0] != 'Document is empty'):
logger.exception('Failed to parse HTML string')
tree = html.Element('div')
for processor in post_processors:
processor(tree)
result = html.tostring(tree, encoding='unicode')
return Markup(result)

View file

@ -0,0 +1,20 @@
from copy import deepcopy
from django.templatetags.static import static
from lxml import html
def lazy_load(tree):
blank = static('blank.gif')
for img in tree.xpath('.//img'):
src = img.get('src', '')
if src.startswith('data') or '-math' in img.get('class', ''):
continue
noscript = html.Element('noscript')
copy = deepcopy(img)
copy.tail = ''
noscript.append(copy)
img.addprevious(noscript)
img.set('data-src', src)
img.set('src', blank)
img.set('class', img.get('class') + ' unveil' if img.get('class') else 'unveil')

View file

@ -0,0 +1,65 @@
import re
import mistune
from judge.utils.mathoid import MathoidMathParser
mistune._pre_tags.append('latex')
class MathInlineGrammar(mistune.InlineGrammar):
block_math = re.compile(r'^\$\$(.*?)\$\$|^\\\[(.*?)\\\]', re.DOTALL)
math = re.compile(r'^~(.*?)~|^\\\((.*?)\\\)', re.DOTALL)
text = re.compile(r'^[\s\S]+?(?=[\\<!\[_*`~$]|\\[\[(]|https?://| {2,}\n|$)')
class MathInlineLexer(mistune.InlineLexer):
grammar_class = MathInlineGrammar
def __init__(self, *args, **kwargs):
self.default_rules = self.default_rules[:]
self.inline_html_rules = self.default_rules
self.default_rules.insert(self.default_rules.index('strikethrough') + 1, 'math')
self.default_rules.insert(self.default_rules.index('strikethrough') + 1, 'block_math')
super(MathInlineLexer, self).__init__(*args, **kwargs)
def output_block_math(self, m):
return self.renderer.block_math(m.group(1) or m.group(2))
def output_math(self, m):
return self.renderer.math(m.group(1) or m.group(2))
def output_inline_html(self, m):
tag = m.group(1)
text = m.group(3)
if self._parse_inline_html and text:
if tag == 'a':
self._in_link = True
text = self.output(text)
self._in_link = False
else:
text = self.output(text)
extra = m.group(2) or ''
html = '<%s%s>%s</%s>' % (tag, extra, text, tag)
else:
html = m.group(0)
return self.renderer.inline_html(html)
class MathRenderer(mistune.Renderer):
def __init__(self, *args, **kwargs):
if kwargs.pop('math', False):
self.mathoid = MathoidMathParser(kwargs.pop('math_engine', None) or 'svg')
else:
self.mathoid = None
super(MathRenderer, self).__init__(*args, **kwargs)
def block_math(self, math):
if self.mathoid is None or not math:
return r'\[%s\]' % mistune.escape(str(math))
return self.mathoid.display_math(math)
def math(self, math):
if self.mathoid is None or not math:
return r'\(%s\)' % mistune.escape(str(math))
return self.mathoid.inline_math(math)

35
judge/jinja2/rating.py Normal file
View file

@ -0,0 +1,35 @@
from django.utils import six
from judge.ratings import rating_class, rating_name, rating_progress
from . import registry
def _get_rating_value(func, obj):
if obj is None:
return None
if isinstance(obj, six.integer_types):
return func(obj)
else:
return func(obj.rating)
@registry.function('rating_class')
def get_rating_class(obj):
return _get_rating_value(rating_class, obj) or 'rate-none'
@registry.function(name='rating_name')
def get_name(obj):
return _get_rating_value(rating_name, obj) or 'Unrated'
@registry.function(name='rating_progress')
def get_progress(obj):
return _get_rating_value(rating_progress, obj) or 0.0
@registry.function
@registry.render_with('user/rating.html')
def rating_number(obj):
return {'rating': obj}

187
judge/jinja2/reference.py Normal file
View file

@ -0,0 +1,187 @@
import re
from collections import defaultdict
from urllib.parse import urljoin
from ansi2html import Ansi2HTMLConverter
from django.contrib.auth.models import AbstractUser
from django.urls import reverse
from django.utils.safestring import mark_safe
from lxml.html import Element
from judge import lxml_tree
from judge.models import Contest, Problem, Profile
from judge.ratings import rating_class, rating_progress
from . import registry
rereference = re.compile(r'\[(r?user):(\w+)\]')
def get_user(username, data):
if not data:
element = Element('span')
element.text = username
return element
element = Element('span', {'class': Profile.get_user_css_class(*data)})
link = Element('a', {'href': reverse('user_page', args=[username])})
link.text = username
element.append(link)
return element
def get_user_rating(username, data):
if not data:
element = Element('span')
element.text = username
return element
rating = data[1]
element = Element('a', {'class': 'rate-group', 'href': reverse('user_page', args=[username])})
if rating:
rating_css = rating_class(rating)
rate_box = Element('span', {'class': 'rate-box ' + rating_css})
rate_box.append(Element('span', {'style': 'height: %3.fem' % rating_progress(rating)}))
user = Element('span', {'class': 'rating ' + rating_css})
user.text = username
element.append(rate_box)
element.append(user)
else:
element.text = username
return element
def get_user_info(usernames):
return {name: (rank, rating) for name, rank, rating in
Profile.objects.filter(user__username__in=usernames)
.values_list('user__username', 'display_rank', 'rating')}
reference_map = {
'user': (get_user, get_user_info),
'ruser': (get_user_rating, get_user_info),
}
def process_reference(text):
# text/tail -> text/tail + elements
last = 0
tail = text
prev = None
elements = []
for piece in rereference.finditer(text):
if prev is None:
tail = text[last:piece.start()]
else:
prev.append(text[last:piece.start()])
prev = list(piece.groups())
elements.append(prev)
last = piece.end()
if prev is not None:
prev.append(text[last:])
return tail, elements
def populate_list(queries, list, element, tail, children):
if children:
for elem in children:
queries[elem[0]].append(elem[1])
list.append((element, tail, children))
def update_tree(list, results, is_tail=False):
for element, text, children in list:
after = []
for type, name, tail in children:
child = reference_map[type][0](name, results[type].get(name))
child.tail = tail
after.append(child)
after = iter(reversed(after))
if is_tail:
element.tail = text
link = element.getnext()
if link is None:
link = next(after)
element.getparent().append(link)
else:
element.text = text
link = next(after)
element.insert(0, link)
for child in after:
link.addprevious(child)
link = child
@registry.filter
def reference(text):
tree = lxml_tree.fromstring(text)
texts = []
tails = []
queries = defaultdict(list)
for element in tree.iter():
if element.text:
populate_list(queries, texts, element, *process_reference(element.text))
if element.tail:
populate_list(queries, tails, element, *process_reference(element.tail))
results = {type: reference_map[type][1](values) for type, values in queries.items()}
update_tree(texts, results, is_tail=False)
update_tree(tails, results, is_tail=True)
return tree
@registry.filter
def item_title(item):
if isinstance(item, Problem):
return item.name
elif isinstance(item, Contest):
return item.name
return '<Unknown>'
@registry.function
@registry.render_with('user/link.html')
def link_user(user):
if isinstance(user, Profile):
user, profile = user.user, user
elif isinstance(user, AbstractUser):
profile = user.profile
elif type(user).__name__ == 'ContestRankingProfile':
user, profile = user.user, user
else:
raise ValueError('Expected profile or user, got %s' % (type(user),))
return {'user': user, 'profile': profile}
@registry.function
@registry.render_with('user/link-list.html')
def link_users(users):
return {'users': users}
@registry.function
@registry.render_with('runtime-version-fragment.html')
def runtime_versions(versions):
return {'runtime_versions': versions}
@registry.filter(name='absolutify')
def absolute_links(text, url):
tree = lxml_tree.fromstring(text)
for anchor in tree.xpath('.//a'):
href = anchor.get('href')
if href:
anchor.set('href', urljoin(url, href))
return tree
@registry.function(name='urljoin')
def join(first, second, *rest):
if not rest:
return urljoin(first, second)
return urljoin(urljoin(first, second), *rest)
@registry.filter(name='ansi2html')
def ansi2html(s):
return mark_safe(Ansi2HTMLConverter(inline=True).convert(s, full=False))

53
judge/jinja2/registry.py Normal file
View file

@ -0,0 +1,53 @@
from django_jinja.library import render_with
globals = {}
tests = {}
filters = {}
extensions = []
__all__ = ['render_with', 'function', 'filter', 'test', 'extension']
def _store_function(store, func, name=None):
if name is None:
name = func.__name__
store[name] = func
def _register_function(store, name, func):
if name is None and func is None:
def decorator(func):
_store_function(store, func)
return func
return decorator
elif name is not None and func is None:
if callable(name):
_store_function(store, name)
return name
else:
def decorator(func):
_store_function(store, func, name)
return func
return decorator
else:
_store_function(store, func, name)
return func
def filter(name=None, func=None):
return _register_function(filters, name, func)
def function(name=None, func=None):
return _register_function(globals, name, func)
def test(name=None, func=None):
return _register_function(tests, name, func)
def extension(cls):
extensions.append(cls)
return cls

27
judge/jinja2/render.py Normal file
View file

@ -0,0 +1,27 @@
from django.template import (Context, Template as DjangoTemplate, TemplateSyntaxError as DjangoTemplateSyntaxError,
VariableDoesNotExist)
from . import registry
MAX_CACHE = 100
django_cache = {}
def compile_template(code):
if code in django_cache:
return django_cache[code]
# If this works for re.compile, it works for us too.
if len(django_cache) > MAX_CACHE:
django_cache.clear()
t = django_cache[code] = DjangoTemplate(code)
return t
@registry.function
def render_django(template, **context):
try:
return compile_template(template).render(Context(context))
except (VariableDoesNotExist, DjangoTemplateSyntaxError):
return 'Error rendering: %r' % template

34
judge/jinja2/social.py Normal file
View file

@ -0,0 +1,34 @@
from django.template.loader import get_template
from django.utils.safestring import mark_safe
from django_social_share.templatetags.social_share import post_to_facebook_url, post_to_gplus_url, post_to_twitter_url
from . import registry
SHARES = [
('post_to_twitter', 'django_social_share/templatetags/post_to_twitter.html', post_to_twitter_url),
('post_to_facebook', 'django_social_share/templatetags/post_to_facebook.html', post_to_facebook_url),
('post_to_gplus', 'django_social_share/templatetags/post_to_gplus.html', post_to_gplus_url),
# For future versions:
# ('post_to_linkedin', 'django_social_share/templatetags/post_to_linkedin.html', post_to_linkedin_url),
# ('post_to_reddit', 'django_social_share/templatetags/post_to_reddit.html', post_to_reddit_url),
]
def make_func(name, template, url_func):
def func(request, *args):
link_text = args[-1]
context = {'request': request, 'link_text': mark_safe(link_text)}
context = url_func(context, *args[:-1])
return mark_safe(get_template(template).render(context))
func.__name__ = name
registry.function(name, func)
for name, template, url_func in SHARES:
make_func(name, template, url_func)
@registry.function
def recaptcha_init(language=None):
return get_template('snowpenguin/recaptcha/recaptcha_init.html').render({'explicit': False, 'language': language})

29
judge/jinja2/spaceless.py Normal file
View file

@ -0,0 +1,29 @@
import re
from jinja2 import Markup, nodes
from jinja2.ext import Extension
class SpacelessExtension(Extension):
"""
Removes whitespace between HTML tags at compile time, including tab and newline characters.
It does not remove whitespace between jinja2 tags or variables. Neither does it remove whitespace between tags
and their text content.
Adapted from coffin:
https://github.com/coffin/coffin/blob/master/coffin/template/defaulttags.py
Adapted from StackOverflow:
https://stackoverflow.com/a/23741298/1090657
"""
tags = {'spaceless'}
def parse(self, parser):
lineno = next(parser.stream).lineno
body = parser.parse_statements(['name:endspaceless'], drop_needle=True)
return nodes.CallBlock(
self.call_method('_strip_spaces', [], [], None, None),
[], [], body,
).set_lineno(lineno)
def _strip_spaces(self, caller=None):
return Markup(re.sub(r'>\s+<', '><', caller().unescape().strip()))

View file

@ -0,0 +1,21 @@
from . import registry
@registry.function
def submission_layout(submission, profile_id, user, editable_problem_ids, completed_problem_ids):
problem_id = submission.problem_id
can_view = False
if problem_id in editable_problem_ids:
can_view = True
if profile_id == submission.user_id:
can_view = True
if user.has_perm('judge.change_submission'):
can_view = True
if submission.problem_id in completed_problem_ids:
can_view |= submission.problem.is_public or profile_id in submission.problem.tester_ids
return can_view

28
judge/jinja2/timedelta.py Normal file
View file

@ -0,0 +1,28 @@
import datetime
from judge.utils.timedelta import nice_repr
from . import registry
@registry.filter
def timedelta(value, display='long'):
if value is None:
return value
return nice_repr(value, display)
@registry.filter
def timestampdelta(value, display='long'):
value = datetime.timedelta(seconds=value)
return timedelta(value, display)
@registry.filter
def seconds(timedelta):
return timedelta.total_seconds()
@registry.filter
@registry.render_with('time-remaining-fragment.html')
def as_countdown(timedelta):
return {'countdown': timedelta}

117
judge/judgeapi.py Normal file
View file

@ -0,0 +1,117 @@
import json
import logging
import socket
import struct
import zlib
from django.conf import settings
from judge import event_poster as event
logger = logging.getLogger('judge.judgeapi')
size_pack = struct.Struct('!I')
def _post_update_submission(submission, done=False):
if submission.problem.is_public:
event.post('submissions', {'type': 'done-submission' if done else 'update-submission',
'id': submission.id,
'contest': submission.contest_key,
'user': submission.user_id, 'problem': submission.problem_id,
'status': submission.status, 'language': submission.language.key})
def judge_request(packet, reply=True):
sock = socket.create_connection(settings.BRIDGED_DJANGO_CONNECT or
settings.BRIDGED_DJANGO_ADDRESS[0])
output = json.dumps(packet, separators=(',', ':'))
output = zlib.compress(output.encode('utf-8'))
writer = sock.makefile('wb')
writer.write(size_pack.pack(len(output)))
writer.write(output)
writer.close()
if reply:
reader = sock.makefile('rb', -1)
input = reader.read(size_pack.size)
if not input:
raise ValueError('Judge did not respond')
length = size_pack.unpack(input)[0]
input = reader.read(length)
if not input:
raise ValueError('Judge did not respond')
reader.close()
sock.close()
result = json.loads(zlib.decompress(input).decode('utf-8'))
return result
def judge_submission(submission, rejudge, batch_rejudge=False):
from .models import ContestSubmission, Submission, SubmissionTestCase
CONTEST_SUBMISSION_PRIORITY = 0
DEFAULT_PRIORITY = 1
REJUDGE_PRIORITY = 2
BATCH_REJUDGE_PRIORITY = 3
updates = {'time': None, 'memory': None, 'points': None, 'result': None, 'error': None,
'was_rejudged': rejudge, 'status': 'QU'}
try:
# This is set proactively; it might get unset in judgecallback's on_grading_begin if the problem doesn't
# actually have pretests stored on the judge.
updates['is_pretested'] = ContestSubmission.objects.filter(submission=submission) \
.values_list('problem__contest__run_pretests_only', flat=True)[0]
except IndexError:
priority = DEFAULT_PRIORITY
else:
priority = CONTEST_SUBMISSION_PRIORITY
# This should prevent double rejudge issues by permitting only the judging of
# QU (which is the initial state) and D (which is the final state).
# Even though the bridge will not queue a submission already being judged,
# we will destroy the current state by deleting all SubmissionTestCase objects.
# However, we can't drop the old state immediately before a submission is set for judging,
# as that would prevent people from knowing a submission is being scheduled for rejudging.
# It is worth noting that this mechanism does not prevent a new rejudge from being scheduled
# while already queued, but that does not lead to data corruption.
if not Submission.objects.filter(id=submission.id).exclude(status__in=('P', 'G')).update(**updates):
return False
SubmissionTestCase.objects.filter(submission_id=submission.id).delete()
try:
response = judge_request({
'name': 'submission-request',
'submission-id': submission.id,
'problem-id': submission.problem.code,
'language': submission.language.key,
'source': submission.source.source,
'priority': BATCH_REJUDGE_PRIORITY if batch_rejudge else REJUDGE_PRIORITY if rejudge else priority,
})
except BaseException:
logger.exception('Failed to send request to judge')
Submission.objects.filter(id=submission.id).update(status='IE')
success = False
else:
if response['name'] != 'submission-received' or response['submission-id'] != submission.id:
Submission.objects.filter(id=submission.id).update(status='IE')
_post_update_submission(submission)
success = True
return success
def disconnect_judge(judge, force=False):
judge_request({'name': 'disconnect-judge', 'judge-id': judge.name, 'force': force}, reply=False)
def abort_submission(submission):
from .models import Submission
response = judge_request({'name': 'terminate-submission', 'submission-id': submission.id})
# This defaults to true, so that in the case the judgelist fails to remove the submission from the queue,
# and returns a bad-request, the submission is not falsely shown as "Aborted" when it will still be judged.
if not response.get('judge-aborted', True):
Submission.objects.filter(id=submission.id).update(status='AB', result='AB')
event.post('sub_%s' % Submission.get_id_secret(submission.id), {'type': 'aborted-submission'})
_post_update_submission(submission, done=True)

59
judge/lxml_tree.py Normal file
View file

@ -0,0 +1,59 @@
import logging
from django.utils.safestring import SafeData, mark_safe
from lxml import html
from lxml.etree import ParserError, XMLSyntaxError
logger = logging.getLogger('judge.html')
class HTMLTreeString(SafeData):
def __init__(self, str):
try:
self._tree = html.fromstring(str, parser=html.HTMLParser(recover=True))
except (XMLSyntaxError, ParserError) as e:
if str and (not isinstance(e, ParserError) or e.args[0] != 'Document is empty'):
logger.exception('Failed to parse HTML string')
self._tree = html.Element('div')
def __getattr__(self, attr):
try:
return getattr(self._tree, attr)
except AttributeError:
return getattr(str(self), attr)
def __setattr__(self, key, value):
if key[0] == '_':
super(HTMLTreeString, self).__setattr__(key, value)
setattr(self._tree, key, value)
def __repr__(self):
return '<HTMLTreeString %r>' % str(self)
def __str__(self):
return mark_safe(html.tostring(self._tree, encoding='unicode'))
def __radd__(self, other):
return other + str(self)
def __add__(self, other):
return str(self) + other
def __getitem__(self, item):
return str(self)[item]
def __getstate__(self):
return str(self)
def __setstate__(self, state):
self._tree = html.fromstring(state)
@property
def tree(self):
return self._tree
def fromstring(str):
if isinstance(str, HTMLTreeString):
return str
return HTMLTreeString(str)

View file

View file

View file

@ -0,0 +1,17 @@
from django.core.management.base import BaseCommand
from judge.models import Judge
class Command(BaseCommand):
help = 'create a judge'
def add_arguments(self, parser):
parser.add_argument('name', help='the name of the judge')
parser.add_argument('auth_key', help='authentication key for the judge')
def handle(self, *args, **options):
judge = Judge()
judge.name = options['name']
judge.auth_key = options['auth_key']
judge.save()

View file

@ -0,0 +1,32 @@
from django.conf import settings
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand
from judge.models import Language, Profile
class Command(BaseCommand):
help = 'creates a user'
def add_arguments(self, parser):
parser.add_argument('name', help='username')
parser.add_argument('email', help='email, not necessary to be resolvable')
parser.add_argument('password', help='password for the user')
parser.add_argument('language', nargs='?', default=settings.DEFAULT_USER_LANGUAGE,
help='default language ID for user')
parser.add_argument('--superuser', action='store_true', default=False,
help="if specified, creates user with superuser privileges")
parser.add_argument('--staff', action='store_true', default=False,
help="if specified, creates user with staff privileges")
def handle(self, *args, **options):
usr = User(username=options['name'], email=options['email'], is_active=True)
usr.set_password(options['password'])
usr.is_superuser = options['superuser']
usr.is_staff = options['staff']
usr.save()
profile = Profile(user=usr)
profile.language = Language.objects.get(key=options['language'])
profile.save()

View file

@ -0,0 +1,16 @@
from django.core.management.base import BaseCommand, CommandError
from judge.utils.camo import client as camo_client
class Command(BaseCommand):
help = 'obtains the camo url for the specified url'
def add_arguments(self, parser):
parser.add_argument('url', help='url to use camo on')
def handle(self, *args, **options):
if camo_client is None:
raise CommandError('Camo not available')
print(camo_client.image_url(options['url']))

View file

@ -0,0 +1,27 @@
from django.core.management.base import BaseCommand, CommandError
from judge.models import Language, LanguageLimit
class Command(BaseCommand):
help = 'allows the problems that allow <source> to be submitted in <target>'
def add_arguments(self, parser):
parser.add_argument('source', help='language to copy from')
parser.add_argument('target', help='language to copy to')
def handle(self, *args, **options):
try:
source = Language.objects.get(key=options['source'])
except Language.DoesNotExist:
raise CommandError('Invalid source language: %s' % options['source'])
try:
target = Language.objects.get(key=options['target'])
except Language.DoesNotExist:
raise CommandError('Invalid target language: %s' % options['target'])
target.problem_set.set(source.problem_set.all())
LanguageLimit.objects.bulk_create(LanguageLimit(problem=ll.problem, language=target, time_limit=ll.time_limit,
memory_limit=ll.memory_limit)
for ll in LanguageLimit.objects.filter(language=source))

View file

@ -0,0 +1,23 @@
from django.core.management.base import BaseCommand
from judge.models import Problem, ProblemGroup, ProblemType
class Command(BaseCommand):
help = 'create an empty problem'
def add_arguments(self, parser):
parser.add_argument('code', help='problem code')
parser.add_argument('name', help='problem title')
parser.add_argument('body', help='problem description')
parser.add_argument('type', help='problem type')
parser.add_argument('group', help='problem group')
def handle(self, *args, **options):
problem = Problem()
problem.code = options['code']
problem.name = options['name']
problem.description = options['body']
problem.group = ProblemGroup.objects.get(name=options['group'])
problem.types = [ProblemType.objects.get(name=options['type'])]
problem.save()

View file

@ -0,0 +1,130 @@
import glob
import io
import os
import sys
from django.conf import settings
from django.core.management import CommandError
from django.core.management.commands.makemessages import Command as MakeMessagesCommand, check_programs
from judge.models import NavigationBar, ProblemType
class Command(MakeMessagesCommand):
def add_arguments(self, parser):
parser.add_argument('--locale', '-l', default=[], dest='locale', action='append',
help='Creates or updates the message files for the given locale(s) (e.g. pt_BR). '
'Can be used multiple times.')
parser.add_argument('--exclude', '-x', default=[], dest='exclude', action='append',
help='Locales to exclude. Default is none. Can be used multiple times.')
parser.add_argument('--all', '-a', action='store_true', dest='all',
default=False, help='Updates the message files for all existing locales.')
parser.add_argument('--no-wrap', action='store_true', dest='no_wrap',
default=False, help="Don't break long message lines into several lines.")
parser.add_argument('--no-obsolete', action='store_true', dest='no_obsolete',
default=False, help="Remove obsolete message strings.")
parser.add_argument('--keep-pot', action='store_true', dest='keep_pot',
default=False, help="Keep .pot file after making messages. Useful when debugging.")
def handle(self, *args, **options):
locale = options.get('locale')
exclude = options.get('exclude')
self.domain = 'dmoj-user'
self.verbosity = options.get('verbosity')
process_all = options.get('all')
# Need to ensure that the i18n framework is enabled
if settings.configured:
settings.USE_I18N = True
else:
settings.configure(USE_I18N=True)
# Avoid messing with mutable class variables
if options.get('no_wrap'):
self.msgmerge_options = self.msgmerge_options[:] + ['--no-wrap']
self.msguniq_options = self.msguniq_options[:] + ['--no-wrap']
self.msgattrib_options = self.msgattrib_options[:] + ['--no-wrap']
self.xgettext_options = self.xgettext_options[:] + ['--no-wrap']
if options.get('no_location'):
self.msgmerge_options = self.msgmerge_options[:] + ['--no-location']
self.msguniq_options = self.msguniq_options[:] + ['--no-location']
self.msgattrib_options = self.msgattrib_options[:] + ['--no-location']
self.xgettext_options = self.xgettext_options[:] + ['--no-location']
self.no_obsolete = options.get('no_obsolete')
self.keep_pot = options.get('keep_pot')
if locale is None and not exclude and not process_all:
raise CommandError("Type '%s help %s' for usage information." % (
os.path.basename(sys.argv[0]), sys.argv[1]))
self.invoked_for_django = False
self.locale_paths = []
self.default_locale_path = None
if os.path.isdir(os.path.join('conf', 'locale')):
self.locale_paths = [os.path.abspath(os.path.join('conf', 'locale'))]
self.default_locale_path = self.locale_paths[0]
self.invoked_for_django = True
else:
self.locale_paths.extend(settings.LOCALE_PATHS)
# Allow to run makemessages inside an app dir
if os.path.isdir('locale'):
self.locale_paths.append(os.path.abspath('locale'))
if self.locale_paths:
self.default_locale_path = self.locale_paths[0]
if not os.path.exists(self.default_locale_path):
os.makedirs(self.default_locale_path)
# Build locale list
locale_dirs = list(filter(os.path.isdir, glob.glob('%s/*' % self.default_locale_path)))
all_locales = list(map(os.path.basename, locale_dirs))
# Account for excluded locales
if process_all:
locales = all_locales
else:
locales = locale or all_locales
locales = set(locales) - set(exclude)
if locales:
check_programs('msguniq', 'msgmerge', 'msgattrib')
check_programs('xgettext')
try:
potfiles = self.build_potfiles()
# Build po files for each selected locale
for locale in locales:
if self.verbosity > 0:
self.stdout.write("processing locale %s\n" % locale)
for potfile in potfiles:
self.write_po_file(potfile, locale)
finally:
if not self.keep_pot:
self.remove_potfiles()
def find_files(self, root):
return []
def _emit_message(self, potfile, string):
potfile.write('''
msgid "%s"
msgstr ""
''' % string.replace('\\', r'\\').replace('\t', '\\t').replace('\n', '\\n').replace('"', '\\"'))
def process_files(self, file_list):
with io.open(os.path.join(self.default_locale_path, 'dmoj-user.pot'), 'w', encoding='utf-8') as potfile:
if self.verbosity > 1:
self.stdout.write('processing navigation bar')
for label in NavigationBar.objects.values_list('label', flat=True):
if self.verbosity > 2:
self.stdout.write('processing navigation item label "%s"\n' % label)
self._emit_message(potfile, label)
if self.verbosity > 1:
self.stdout.write('processing problem types')
for name in ProblemType.objects.values_list('full_name', flat=True):
if self.verbosity > 2:
self.stdout.write('processing problem type name "%s"\n' % name)
self._emit_message(potfile, name)

View file

@ -0,0 +1,58 @@
import os
import shutil
import sys
from django.conf import settings
from django.core.management.base import BaseCommand
from django.template.loader import get_template
from django.utils import translation
from judge.models import Problem, ProblemTranslation
from judge.pdf_problems import DefaultPdfMaker, PhantomJSPdfMaker, PuppeteerPDFRender, SlimerJSPdfMaker
class Command(BaseCommand):
help = 'renders a PDF file of a problem'
def add_arguments(self, parser):
parser.add_argument('code', help='code of problem to render')
parser.add_argument('directory', nargs='?', help='directory to store temporaries')
parser.add_argument('-l', '--language', default=settings.LANGUAGE_CODE,
help='language to render PDF in')
parser.add_argument('-p', '--phantomjs', action='store_const', const=PhantomJSPdfMaker,
default=DefaultPdfMaker, dest='engine')
parser.add_argument('-s', '--slimerjs', action='store_const', const=SlimerJSPdfMaker, dest='engine')
parser.add_argument('-c', '--chrome', '--puppeteer', action='store_const',
const=PuppeteerPDFRender, dest='engine')
def handle(self, *args, **options):
try:
problem = Problem.objects.get(code=options['code'])
except Problem.DoesNotExist:
print('Bad problem code')
return
try:
trans = problem.translations.get(language=options['language'])
except ProblemTranslation.DoesNotExist:
trans = None
directory = options['directory']
with options['engine'](directory, clean_up=directory is None) as maker, \
translation.override(options['language']):
problem_name = problem.name if trans is None else trans.name
maker.html = get_template('problem/raw.html').render({
'problem': problem,
'problem_name': problem_name,
'description': problem.description if trans is None else trans.description,
'url': '',
'math_engine': maker.math_engine,
}).replace('"//', '"https://').replace("'//", "'https://")
maker.title = problem_name
for file in ('style.css', 'pygment-github.css', 'mathjax_config.js'):
maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file))
maker.make(debug=True)
if not maker.success:
print(maker.log, file=sys.stderr)
elif directory is None:
shutil.move(maker.pdffile, problem.code + '.pdf')

View file

@ -0,0 +1,33 @@
import threading
from django.conf import settings
from django.core.management.base import BaseCommand
from judge.bridge import DjangoHandler, DjangoServer
from judge.bridge import DjangoJudgeHandler, JudgeServer
class Command(BaseCommand):
def handle(self, *args, **options):
judge_handler = DjangoJudgeHandler
try:
import netaddr # noqa: F401, imported to see if it exists
except ImportError:
pass
else:
proxies = settings.BRIDGED_JUDGE_PROXIES
if proxies:
judge_handler = judge_handler.with_proxy_set(proxies)
judge_server = JudgeServer(settings.BRIDGED_JUDGE_ADDRESS, judge_handler)
django_server = DjangoServer(judge_server.judges, settings.BRIDGED_DJANGO_ADDRESS, DjangoHandler)
# TODO: Merge the two servers
threading.Thread(target=django_server.serve_forever).start()
try:
judge_server.serve_forever()
except KeyboardInterrupt:
pass
finally:
django_server.stop()

View file

@ -0,0 +1,53 @@
from django.conf import settings
from django.core.management.base import BaseCommand
from moss import *
from judge.models import Contest, ContestParticipation, Submission
class Command(BaseCommand):
help = 'Checks for duplicate code using MOSS'
LANG_MAPPING = {
('C++', MOSS_LANG_CC),
('C', MOSS_LANG_C),
('Java', MOSS_LANG_JAVA),
('Python', MOSS_LANG_PYTHON),
}
def add_arguments(self, parser):
parser.add_argument('contest', help='the id of the contest')
def handle(self, *args, **options):
moss_api_key = settings.MOSS_API_KEY
if moss_api_key is None:
print('No MOSS API Key supplied')
return
contest = options['contest']
for problem in Contest.objects.get(key=contest).problems.order_by('code'):
print('========== %s / %s ==========' % (problem.code, problem.name))
for dmoj_lang, moss_lang in self.LANG_MAPPING:
print("%s: " % dmoj_lang, end=' ')
subs = Submission.objects.filter(
contest__participation__virtual__in=(ContestParticipation.LIVE, ContestParticipation.SPECTATE),
contest__participation__contest__key=contest,
result='AC', problem__id=problem.id,
language__common_name=dmoj_lang,
).values_list('user__user__username', 'source__source')
if not subs:
print('<no submissions>')
continue
moss_call = MOSS(moss_api_key, language=moss_lang, matching_file_limit=100,
comment='%s - %s' % (contest, problem.code))
users = set()
for username, source in subs:
if username in users:
continue
users.add(username)
moss_call.add_file_from_memory(username, source.encode('utf-8'))
print('(%d): %s' % (subs.count(), moss_call.process()))

63
judge/middleware.py Normal file
View file

@ -0,0 +1,63 @@
from django.conf import settings
from django.http import HttpResponseRedirect
from django.urls import Resolver404, resolve, reverse
from django.utils.http import urlquote
class ShortCircuitMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
try:
callback, args, kwargs = resolve(request.path_info, getattr(request, 'urlconf', None))
except Resolver404:
callback, args, kwargs = None, None, None
if getattr(callback, 'short_circuit_middleware', False):
return callback(request, *args, **kwargs)
return self.get_response(request)
class DMOJLoginMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.user.is_authenticated:
profile = request.profile = request.user.profile
login_2fa_path = reverse('login_2fa')
if (profile.is_totp_enabled and not request.session.get('2fa_passed', False) and
request.path not in (login_2fa_path, reverse('auth_logout')) and
not request.path.startswith(settings.STATIC_URL)):
return HttpResponseRedirect(login_2fa_path + '?next=' + urlquote(request.get_full_path()))
else:
request.profile = None
return self.get_response(request)
class DMOJImpersonationMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.user.is_impersonate:
request.no_profile_update = True
request.profile = request.user.profile
return self.get_response(request)
class ContestMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
profile = request.profile
if profile:
profile.update_contest()
request.participation = profile.current_contest
request.in_contest = request.participation is not None
else:
request.in_contest = False
request.participation = None
return self.get_response(request)

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.15 on 2019-01-31 22:18
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('judge', '0084_contest_formats'),
]
operations = [
migrations.CreateModel(
name='SubmissionSource',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('source', models.TextField(max_length=65536, verbose_name='source code')),
('submission', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='link', to='judge.Submission', verbose_name='associated submission')),
],
),
migrations.RunSQL(
['''INSERT INTO judge_submissionsource (source, submission_id)
SELECT source, id AS 'submission_id' FROM judge_submission;'''],
['''UPDATE judge_submission sub
INNER JOIN judge_submissionsource src ON sub.id = src.submission_id
SET sub.source = src.source;'''],
elidable=True,
),
migrations.RemoveField(
model_name='submission',
name='source',
),
migrations.AlterField(
model_name='submissionsource',
name='submission',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='source', to='judge.Submission', verbose_name='associated submission'),
),
]

View file

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-06-20 16:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('judge', '0085_submission_source'),
]
operations = [
migrations.AddField(
model_name='contest',
name='rating_ceiling',
field=models.IntegerField(blank=True, help_text='Rating ceiling for contest', null=True, verbose_name='rating ceiling'),
),
migrations.AddField(
model_name='contest',
name='rating_floor',
field=models.IntegerField(blank=True, help_text='Rating floor for contest', null=True, verbose_name='rating floor'),
),
]

View file

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-06-09 14:44
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('judge', '0086_rating_ceiling'),
]
operations = [
migrations.AlterField(
model_name='problem',
name='memory_limit',
field=models.PositiveIntegerField(help_text='The memory limit for this problem, in kilobytes (e.g. 64mb = 65536 kilobytes).', verbose_name='memory limit'),
),
migrations.AlterField(
model_name='problem',
name='time_limit',
field=models.FloatField(help_text='The time limit for this problem, in seconds. Fractional seconds (e.g. 1.5) are supported.', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(2000)], verbose_name='time limit'),
),
]

View file

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-09-02 15:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('judge', '0087_problem_resource_limits'),
]
operations = [
migrations.AlterModelOptions(
name='contest',
options={'permissions': (('see_private_contest', 'See private contests'), ('edit_own_contest', 'Edit own contests'), ('edit_all_contest', 'Edit all contests'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes'), ('create_private_contest', 'Create private contests')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'},
),
migrations.RenameField(
model_name='contest',
old_name='is_public',
new_name='is_visible',
),
migrations.AddField(
model_name='contest',
name='is_organization_private',
field=models.BooleanField(default=False, verbose_name='private to organizations'),
),
migrations.AddField(
model_name='contest',
name='private_contestants',
field=models.ManyToManyField(blank=True, help_text='If private, only these users may see the contest', related_name='_contest_private_contestants_+', to='judge.Profile', verbose_name='private contestants'),
),
migrations.AlterField(
model_name='contest',
name='is_private',
field=models.BooleanField(default=False, verbose_name='private to specific users'),
),
]

View file

@ -0,0 +1,27 @@
# Generated by Django 2.1.12 on 2019-09-25 23:28
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('judge', '0088_private_contests'),
]
operations = [
migrations.AddField(
model_name='submission',
name='contest_object',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='judge.Contest', verbose_name='contest'),
),
migrations.RunSQL('''
UPDATE `judge_submission`
INNER JOIN `judge_contestsubmission`
ON (`judge_submission`.`id` = `judge_contestsubmission`.`submission_id`)
INNER JOIN `judge_contestparticipation`
ON (`judge_contestsubmission`.`participation_id` = `judge_contestparticipation`.`id`)
SET `judge_submission`.`contest_object_id` = `judge_contestparticipation`.`contest_id`
''', migrations.RunSQL.noop),
]

View file

@ -0,0 +1,19 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('judge', '0089_submission_to_contest'),
]
operations = [
migrations.RunSQL('''
UPDATE `judge_contest`
SET `judge_contest`.`is_private` = 0, `judge_contest`.`is_organization_private` = 1
WHERE `judge_contest`.`is_private` = 1
''', '''
UPDATE `judge_contest`
SET `judge_contest`.`is_private` = `judge_contest`.`is_organization_private`
'''),
]

View file

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
import lxml.html as lh
from django.db import migrations
from lxml.html.clean import clean_html
def strip_error_html(apps, schema_editor):
Submission = apps.get_model('judge', 'Submission')
for sub in Submission.objects.filter(error__isnull=False).iterator():
if sub.error:
sub.error = clean_html(lh.fromstring(sub.error)).text_content()
sub.save(update_fields=['error'])
class Migration(migrations.Migration):
dependencies = [
('judge', '0090_fix_contest_visibility'),
]
operations = [
migrations.RunPython(strip_error_html, migrations.RunPython.noop, atomic=True),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 2.1.12 on 2019-09-30 21:25
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('judge', '0091_compiler_message_ansi2html'),
]
operations = [
migrations.AlterModelOptions(
name='contest',
options={'permissions': (('see_private_contest', 'See private contests'), ('edit_own_contest', 'Edit own contests'), ('edit_all_contest', 'Edit all contests'), ('clone_contest', 'Clone contest'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes'), ('create_private_contest', 'Create private contests')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'},
),
]

View file

@ -0,0 +1,45 @@
# Generated by Django 2.1.12 on 2019-10-17 20:52
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('judge', '0092_contest_clone'),
]
operations = [
migrations.CreateModel(
name='ContestMoss',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('language', models.CharField(max_length=10)),
('submission_count', models.PositiveIntegerField(default=0)),
('url', models.URLField(blank=True, null=True)),
],
options={
'verbose_name': 'contest moss result',
'verbose_name_plural': 'contest moss results',
},
),
migrations.AlterModelOptions(
name='contest',
options={'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'},
),
migrations.AddField(
model_name='contestmoss',
name='contest',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moss', to='judge.Contest', verbose_name='contest'),
),
migrations.AddField(
model_name='contestmoss',
name='problem',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moss', to='judge.Problem', verbose_name='problem'),
),
migrations.AlterUniqueTogether(
name='contestmoss',
unique_together={('contest', 'problem', 'language')},
),
]

View file

@ -0,0 +1,14 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('judge', '0093_contest_moss'),
]
operations = [
migrations.AlterUniqueTogether(
name='submissiontestcase',
unique_together={('submission', 'case')},
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 2.1.12 on 2019-10-23 00:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('judge', '0094_submissiontestcase_unique_together'),
]
operations = [
migrations.AddField(
model_name='organization',
name='logo_override_image',
field=models.CharField(blank=True, default='', help_text='This image will replace the default site logo for users viewing the organization.', max_length=150, verbose_name='Logo override image'),
),
]

View file

@ -0,0 +1,21 @@
# Generated by Django 2.2.6 on 2019-11-08 01:27
import django.db.models.deletion
from django.db import migrations, models
import judge.models.runtime
class Migration(migrations.Migration):
dependencies = [
('judge', '0095_organization_logo_override'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='language',
field=models.ForeignKey(default=judge.models.runtime.Language.get_default_language_pk, on_delete=django.db.models.deletion.SET_DEFAULT, to='judge.Language', verbose_name='preferred language'),
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 2.2.7 on 2019-11-10 02:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('judge', '0096_profile_language_set_default'),
]
operations = [
migrations.AddField(
model_name='contestparticipation',
name='is_disqualified',
field=models.BooleanField(default=False, help_text='Whether this participation is disqualified.', verbose_name='is disqualified'),
),
migrations.AlterField(
model_name='contestparticipation',
name='virtual',
field=models.IntegerField(default=0, help_text='0 means non-virtual, otherwise the n-th virtual participation.', verbose_name='virtual participation id'),
),
]

View file

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)

268
judge/pdf_problems.py Normal file
View file

@ -0,0 +1,268 @@
import errno
import io
import json
import logging
import os
import shutil
import subprocess
import uuid
from django.conf import settings
from django.utils.translation import gettext
HAS_PHANTOMJS = os.access(settings.PHANTOMJS, os.X_OK)
HAS_SLIMERJS = os.access(settings.SLIMERJS, os.X_OK)
NODE_PATH = settings.NODEJS
PUPPETEER_MODULE = settings.PUPPETEER_MODULE
HAS_PUPPETEER = os.access(NODE_PATH, os.X_OK) and os.path.isdir(PUPPETEER_MODULE)
HAS_PDF = (os.path.isdir(settings.DMOJ_PDF_PROBLEM_CACHE) and
(HAS_PHANTOMJS or HAS_SLIMERJS or HAS_PUPPETEER))
EXIFTOOL = settings.EXIFTOOL
HAS_EXIFTOOL = os.access(EXIFTOOL, os.X_OK)
logger = logging.getLogger('judge.problem.pdf')
class BasePdfMaker(object):
math_engine = 'jax'
title = None
def __init__(self, dir=None, clean_up=True):
self.dir = dir or os.path.join(settings.DMOJ_PDF_PROBLEM_TEMP_DIR, str(uuid.uuid1()))
self.proc = None
self.log = None
self.htmlfile = os.path.join(self.dir, 'input.html')
self.pdffile = os.path.join(self.dir, 'output.pdf')
self.clean_up = clean_up
def load(self, file, source):
with open(os.path.join(self.dir, file), 'w') as target, open(source) as source:
target.write(source.read())
def make(self, debug=False):
self._make(debug)
if self.title and HAS_EXIFTOOL:
try:
subprocess.check_output([EXIFTOOL, '-Title=%s' % (self.title,), self.pdffile])
except subprocess.CalledProcessError as e:
logger.error('Failed to run exiftool to set title for: %s\n%s', self.title, e.output)
def _make(self, debug):
raise NotImplementedError()
@property
def html(self):
with io.open(self.htmlfile, encoding='utf-8') as f:
return f.read()
@html.setter
def html(self, data):
with io.open(self.htmlfile, 'w', encoding='utf-8') as f:
f.write(data)
@property
def success(self):
return self.proc.returncode == 0
@property
def created(self):
return os.path.exists(self.pdffile)
def __enter__(self):
try:
os.makedirs(self.dir)
except OSError as e:
if e.errno != errno.EEXIST:
raise
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if self.clean_up:
shutil.rmtree(self.dir, ignore_errors=True)
class PhantomJSPdfMaker(BasePdfMaker):
template = '''\
"use strict";
var page = require('webpage').create();
var param = {params};
page.paperSize = {
format: param.paper, orientation: 'portrait', margin: '1cm',
footer: {
height: '1cm',
contents: phantom.callback(function(num, pages) {
return ('<center style="margin: 0 auto; font-family: Segoe UI; font-size: 10px">'
+ param.footer.replace('[page]', num).replace('[topage]', pages) + '</center>');
})
}
};
page.onCallback = function (data) {
if (data.action === 'snapshot') {
page.render(param.output);
phantom.exit();
}
}
page.open(param.input, function (status) {
if (status !== 'success') {
console.log('Unable to load the address!');
phantom.exit(1);
} else {
page.evaluate(function (zoom) {
document.documentElement.style.zoom = zoom;
}, param.zoom);
window.setTimeout(function () {
page.render(param.output);
phantom.exit();
}, param.timeout);
}
});
'''
def get_render_script(self):
return self.template.replace('{params}', json.dumps({
'zoom': settings.PHANTOMJS_PDF_ZOOM,
'timeout': int(settings.PHANTOMJS_PDF_TIMEOUT * 1000),
'input': 'input.html', 'output': 'output.pdf',
'paper': settings.PHANTOMJS_PAPER_SIZE,
'footer': gettext('Page [page] of [topage]'),
}))
def _make(self, debug):
with io.open(os.path.join(self.dir, '_render.js'), 'w', encoding='utf-8') as f:
f.write(self.get_render_script())
cmdline = [settings.PHANTOMJS, '_render.js']
self.proc = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.dir)
self.log = self.proc.communicate()[0]
class SlimerJSPdfMaker(BasePdfMaker):
math_engine = 'mml'
template = '''\
"use strict";
try {
var param = {params};
var {Cc, Ci} = require('chrome');
var prefs = Cc['@mozilla.org/preferences-service;1'].getService(Ci.nsIPrefService);
// Changing the serif font so that printed footers show up as Segoe UI.
var branch = prefs.getBranch('font.name.serif.');
branch.setCharPref('x-western', 'Segoe UI');
var page = require('webpage').create();
page.paperSize = {
format: param.paper, orientation: 'portrait', margin: '1cm', edge: '0.5cm',
footerStr: { left: '', right: '', center: param.footer }
};
page.open(param.input, function (status) {
if (status !== 'success') {
console.log('Unable to load the address!');
slimer.exit(1);
} else {
page.render(param.output, { ratio: param.zoom });
slimer.exit();
}
});
} catch (e) {
console.error(e);
slimer.exit(1);
}
'''
def get_render_script(self):
return self.template.replace('{params}', json.dumps({
'zoom': settings.SLIMERJS_PDF_ZOOM,
'input': 'input.html', 'output': 'output.pdf',
'paper': settings.SLIMERJS_PAPER_SIZE,
'footer': gettext('Page [page] of [topage]').replace('[page]', '&P').replace('[topage]', '&L'),
}))
def _make(self, debug):
with io.open(os.path.join(self.dir, '_render.js'), 'w', encoding='utf-8') as f:
f.write(self.get_render_script())
env = None
firefox = settings.SLIMERJS_FIREFOX_PATH
if firefox:
env = os.environ.copy()
env['SLIMERJSLAUNCHER'] = firefox
cmdline = [settings.SLIMERJS, '--headless', '_render.js']
self.proc = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.dir, env=env)
self.log = self.proc.communicate()[0]
class PuppeteerPDFRender(BasePdfMaker):
template = '''\
"use strict";
const param = {params};
const puppeteer = require('puppeteer');
puppeteer.launch().then(browser => Promise.resolve()
.then(async () => {
const page = await browser.newPage();
await page.goto(param.input, { waitUntil: 'load' });
await page.waitForSelector('.math-loaded', { timeout: 15000 });
await page.pdf({
path: param.output,
format: param.paper,
margin: {
top: '1cm',
bottom: '1cm',
left: '1cm',
right: '1cm',
},
printBackground: true,
displayHeaderFooter: true,
headerTemplate: '<div></div>',
footerTemplate: '<center style="margin: 0 auto; font-family: Segoe UI; font-size: 10px">' +
param.footer.replace('[page]', '<span class="pageNumber"></span>')
.replace('[topage]', '<span class="totalPages"></span>')
+ '</center>',
});
await browser.close();
})
.catch(e => browser.close().then(() => {throw e}))
).catch(e => {
console.error(e);
process.exit(1);
});
'''
def get_render_script(self):
return self.template.replace('{params}', json.dumps({
'input': 'file://' + os.path.abspath(os.path.join(self.dir, 'input.html')),
'output': os.path.abspath(os.path.join(self.dir, 'output.pdf')),
'paper': settings.PUPPETEER_PAPER_SIZE,
'footer': gettext('Page [page] of [topage]'),
}))
def _make(self, debug):
with io.open(os.path.join(self.dir, '_render.js'), 'w', encoding='utf-8') as f:
f.write(self.get_render_script())
env = os.environ.copy()
env['NODE_PATH'] = os.path.dirname(PUPPETEER_MODULE)
cmdline = [NODE_PATH, '_render.js']
self.proc = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.dir, env=env)
self.log = self.proc.communicate()[0]
if HAS_PUPPETEER:
DefaultPdfMaker = PuppeteerPDFRender
elif HAS_SLIMERJS:
DefaultPdfMaker = SlimerJSPdfMaker
elif HAS_PHANTOMJS:
DefaultPdfMaker = PhantomJSPdfMaker
else:
DefaultPdfMaker = None

View file

@ -0,0 +1,78 @@
from collections import namedtuple
from django.conf import settings
from django.db import connection
from judge.models import Submission
from judge.timezone import from_database_time
PP_WEIGHT_TABLE = [pow(settings.DMOJ_PP_STEP, i) for i in range(settings.DMOJ_PP_ENTRIES)]
PPBreakdown = namedtuple('PPBreakdown', 'points weight scaled_points problem_name problem_code '
'sub_id sub_date sub_points sub_total sub_result_class '
'sub_short_status sub_long_status sub_lang')
def get_pp_breakdown(user, start=0, end=settings.DMOJ_PP_ENTRIES):
with connection.cursor() as cursor:
cursor.execute('''
SELECT max_points_table.problem_code,
max_points_table.problem_name,
max_points_table.max_points,
judge_submission.id,
judge_submission.date,
judge_submission.case_points,
judge_submission.case_total,
judge_submission.result,
judge_language.short_name,
judge_language.key
FROM judge_submission
JOIN (SELECT judge_problem.id problem_id,
judge_problem.name problem_name,
judge_problem.code problem_code,
MAX(judge_submission.points) AS max_points
FROM judge_problem
INNER JOIN judge_submission ON (judge_problem.id = judge_submission.problem_id)
WHERE (judge_problem.is_public = True AND
judge_problem.is_organization_private = False AND
judge_submission.points IS NOT NULL AND
judge_submission.user_id = %s)
GROUP BY judge_problem.id
HAVING MAX(judge_submission.points) > 0.0) AS max_points_table
ON (judge_submission.problem_id = max_points_table.problem_id AND
judge_submission.points = max_points_table.max_points AND
judge_submission.user_id = %s)
JOIN judge_language
ON judge_submission.language_id = judge_language.id
GROUP BY max_points_table.problem_id
ORDER BY max_points DESC, judge_submission.date DESC
LIMIT %s OFFSET %s
''', (user.id, user.id, end - start + 1, start))
data = cursor.fetchall()
breakdown = []
for weight, contrib in zip(PP_WEIGHT_TABLE[start:end], data):
code, name, points, id, date, case_points, case_total, result, lang_short_name, lang_key = contrib
# Replicates a lot of the logic usually done on Submission objects
lang_short_display_name = lang_short_name or lang_key
result_class = Submission.result_class_from_code(result, case_points, case_total)
long_status = Submission.USER_DISPLAY_CODES.get(result, '')
breakdown.append(PPBreakdown(
points=points,
weight=weight * 100,
scaled_points=points * weight,
problem_name=name,
problem_code=code,
sub_id=id,
sub_date=from_database_time(date),
sub_points=case_points,
sub_total=case_total,
sub_short_status=result,
sub_long_status=long_status,
sub_result_class=result_class,
sub_lang=lang_short_display_name,
))
has_more = end < min(len(PP_WEIGHT_TABLE), start + len(data))
return breakdown, has_more

Some files were not shown because too many files have changed in this diff Show more